UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

419 lines (380 loc) 16.7 kB
import _typeof from "@babel/runtime/helpers/esm/typeof"; import _createForOfIteratorHelper from "@babel/runtime/helpers/esm/createForOfIteratorHelper"; import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray"; import * as React from 'react'; import { stringOrHastToJsx } from "../pipeline/hastUtils/index.js"; import { useUrlHashState } from "../useUrlHashState/index.js"; import { countLines } from "../pipeline/parseSource/addLineGutters.js"; /** * Converts a string to kebab-case * @param str - The string to convert * @returns kebab-case string */ function toKebabCase(str) { return str // Insert a dash before any uppercase letter that follows a lowercase letter or digit .replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+|-+$/g, ''); } /** * Generates a file slug based on main slug, file name, and variant name * @param mainSlug - The main component/demo slug * @param fileName - The file name * @param variantName - The variant name * @param isInitialVariant - Whether this is the initial/default variant * @returns Generated file slug */ function generateFileSlug(mainSlug, fileName, variantName, isInitialVariant) { // Extract base name from filename (strip extension) var lastDotIndex = fileName.lastIndexOf('.'); var baseName = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName; var extension = lastDotIndex !== -1 ? fileName.substring(lastDotIndex) : ''; // Convert to kebab-case var kebabMainSlug = toKebabCase(mainSlug); var kebabBaseName = toKebabCase(baseName); var kebabVariantName = toKebabCase(variantName); // Reconstruct filename with kebab-case base name but preserved extension var kebabFileName = "".concat(kebabBaseName).concat(extension); // Handle empty main slug case if (!kebabMainSlug) { return kebabFileName; } // Format: mainSlug:fileName.ext (for initial variant) or mainSlug:variantName:fileName.ext if (isInitialVariant) { return "".concat(kebabMainSlug, ":").concat(kebabFileName); } return "".concat(kebabMainSlug, ":").concat(kebabVariantName, ":").concat(kebabFileName); } /** * Hook for managing file selection and navigation within a code variant */ export function useFileNavigation(_ref) { var selectedVariant = _ref.selectedVariant, transformedFiles = _ref.transformedFiles, _ref$mainSlug = _ref.mainSlug, mainSlug = _ref$mainSlug === void 0 ? '' : _ref$mainSlug, _ref$selectedVariantK = _ref.selectedVariantKey, selectedVariantKey = _ref$selectedVariantK === void 0 ? '' : _ref$selectedVariantK, _ref$variantKeys = _ref.variantKeys, variantKeys = _ref$variantKeys === void 0 ? [] : _ref$variantKeys, initialVariant = _ref.initialVariant, shouldHighlight = _ref.shouldHighlight; // Keep selectedFileName as untransformed filename for internal tracking var _React$useState = React.useState(selectedVariant == null ? void 0 : selectedVariant.fileName), _React$useState2 = _slicedToArray(_React$useState, 2), selectedFileNameInternal = _React$useState2[0], setSelectedFileNameInternal = _React$useState2[1]; // Use the new URL hash hook var _useUrlHashState = useUrlHashState(), hash = _useUrlHashState.hash, setHash = _useUrlHashState.setHash, hasUserInteraction = _useUrlHashState.hasUserInteraction, markUserInteraction = _useUrlHashState.markUserInteraction; // Helper function to check URL hash and switch to matching file var checkUrlHashAndSelectFile = React.useCallback(function () { if (!selectedVariant || !hash) { return; } // Check if hash matches any file slug var matchingFileName; // Determine if this is the initial variant var isInitialVariant = initialVariant ? selectedVariantKey === initialVariant : variantKeys.length === 0 || selectedVariantKey === variantKeys[0]; // Check main file if (selectedVariant.fileName) { var mainFileSlug = generateFileSlug(mainSlug, selectedVariant.fileName, selectedVariantKey, isInitialVariant); if (hash === mainFileSlug) { matchingFileName = selectedVariant.fileName; } } // Check extra files if (!matchingFileName && selectedVariant.extraFiles) { for (var _i = 0, _Object$keys = Object.keys(selectedVariant.extraFiles); _i < _Object$keys.length; _i++) { var fileName = _Object$keys[_i]; var fileSlug = generateFileSlug(mainSlug, fileName, selectedVariantKey, isInitialVariant); if (hash === fileSlug) { matchingFileName = fileName; break; } } } // Check transformed files if available if (!matchingFileName && transformedFiles) { var _iterator = _createForOfIteratorHelper(transformedFiles.files), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var file = _step.value; var _fileSlug = generateFileSlug(mainSlug, file.originalName, selectedVariantKey, isInitialVariant); if (hash === _fileSlug) { matchingFileName = file.originalName; break; } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } if (matchingFileName) { setSelectedFileNameInternal(matchingFileName); markUserInteraction(); // Mark that user has made a selection via URL } }, [selectedVariant, hash, transformedFiles, mainSlug, selectedVariantKey, variantKeys, initialVariant, markUserInteraction]); // On hydration/variant change, check URL hash and switch to matching file React.useEffect(function () { checkUrlHashAndSelectFile(); }, [checkUrlHashAndSelectFile]); // Reset selectedFileName when variant changes React.useEffect(function () { if (selectedVariant && selectedFileNameInternal !== selectedVariant.fileName) { // Only reset if current selectedFileName doesn't exist in the new variant var hasFile = selectedVariant.fileName === selectedFileNameInternal || selectedFileNameInternal && selectedVariant.extraFiles && selectedVariant.extraFiles[selectedFileNameInternal]; if (!hasFile) { setSelectedFileNameInternal(selectedVariant.fileName); } } }, [selectedVariant, selectedFileNameInternal]); // Update URL when variant changes (to reflect new slug for current file) React.useEffect(function () { if (!selectedVariant || typeof window === 'undefined' || !selectedFileNameInternal || !hasUserInteraction) { return; } // Determine if this is the initial variant var isInitialVariant = initialVariant ? selectedVariantKey === initialVariant : variantKeys.length === 0 || selectedVariantKey === variantKeys[0]; // Generate the new slug for the currently selected file var fileSlug = ''; if (transformedFiles) { var file = transformedFiles.files.find(function (f) { return f.originalName === selectedFileNameInternal; }); if (file) { fileSlug = generateFileSlug(mainSlug, file.originalName, selectedVariantKey, isInitialVariant); } } else { fileSlug = generateFileSlug(mainSlug, selectedFileNameInternal, selectedVariantKey, isInitialVariant); } // Update the URL hash without adding to history (replaceState) if (fileSlug) { setHash(fileSlug); // Use the new URL hash hook } }, [selectedVariant, selectedFileNameInternal, transformedFiles, mainSlug, selectedVariantKey, variantKeys, initialVariant, hasUserInteraction, setHash]); // Compute the displayed filename (transformed if applicable) var selectedFileName = React.useMemo(function () { if (!selectedVariant) { return undefined; } // If selectedFileNameInternal is undefined, we're selecting the main file var effectiveFileName = selectedFileNameInternal || selectedVariant.fileName; if (!effectiveFileName) { return undefined; } // If we have transformed files, return the transformed name if (transformedFiles) { var file = transformedFiles.files.find(function (f) { return f.originalName === effectiveFileName; }); return file ? file.name : effectiveFileName; } // Otherwise, return the original filename return effectiveFileName; }, [selectedVariant, selectedFileNameInternal, transformedFiles]); var selectedFile = React.useMemo(function () { if (!selectedVariant) { return null; } // If we have transformed files, use them if (transformedFiles) { var effectiveFileName = selectedFileNameInternal || selectedVariant.fileName; var file = transformedFiles.files.find(function (f) { return f.originalName === effectiveFileName; }); return file ? file.source : null; } // Otherwise, use the original untransformed files if (selectedFileNameInternal === selectedVariant.fileName || !selectedFileNameInternal) { var _selectedVariant$sour; return (_selectedVariant$sour = selectedVariant.source) != null ? _selectedVariant$sour : null; } // Look in extraFiles if (selectedFileNameInternal && selectedVariant.extraFiles && selectedVariant.extraFiles[selectedFileNameInternal]) { var extraFile = selectedVariant.extraFiles[selectedFileNameInternal]; if (typeof extraFile === 'string') { return extraFile; } if (extraFile && _typeof(extraFile) === 'object' && 'source' in extraFile) { var _extraFile$source; return (_extraFile$source = extraFile.source) != null ? _extraFile$source : null; } } return null; }, [selectedVariant, selectedFileNameInternal, transformedFiles]); var selectedFileComponent = React.useMemo(function () { if (!selectedVariant) { return null; } // If we have transformed files, use them if (transformedFiles) { var file = transformedFiles.files.find(function (f) { return f.originalName === selectedFileNameInternal; }); return file ? file.component : null; } // Otherwise, create component from original untransformed files if (selectedFileNameInternal === selectedVariant.fileName || !selectedFileNameInternal) { if (selectedVariant.source == null) { return null; } return stringOrHastToJsx(selectedVariant.source, shouldHighlight); } // Look in extraFiles if (selectedFileNameInternal && selectedVariant.extraFiles && selectedVariant.extraFiles[selectedFileNameInternal]) { var extraFile = selectedVariant.extraFiles[selectedFileNameInternal]; var source; if (typeof extraFile === 'string') { source = extraFile; } else if (extraFile && _typeof(extraFile) === 'object' && 'source' in extraFile) { source = extraFile.source; } else { return null; } if (source == null) { return null; } return stringOrHastToJsx(source, shouldHighlight); } return null; }, [selectedVariant, selectedFileNameInternal, transformedFiles, shouldHighlight]); var selectedFileLines = React.useMemo(function () { if (selectedFile == null) { return 0; } // If it's a string, split by newlines and count if (typeof selectedFile === 'string') { return selectedFile.split('\n').length; } // If it's a hast object, count the children length if (selectedFile && _typeof(selectedFile) === 'object') { var hastSelectedFile; if ('hastJson' in selectedFile) { hastSelectedFile = JSON.parse(selectedFile.hastJson); } else { hastSelectedFile = selectedFile; } if (hastSelectedFile.data && 'totalLines' in hastSelectedFile.data) { var totalLines = hastSelectedFile.data.totalLines; // Check if totalLines is a valid number (not null, undefined, or NaN) if (totalLines != null && !Number.isNaN(Number(totalLines))) { var numLines = Number(totalLines); if (numLines >= 0) { return numLines; } } // Fall through to children count if totalLines is invalid } if ('children' in hastSelectedFile) { // Use countLines for more accurate line counting of HAST trees return countLines(hastSelectedFile); } } return 0; }, [selectedFile]); // Convert files for the return interface var files = React.useMemo(function () { if (!selectedVariant) { return []; } // Determine if this is the initial variant var isInitialVariant = initialVariant ? selectedVariantKey === initialVariant : variantKeys.length === 0 || selectedVariantKey === variantKeys[0]; // If we have transformed files, use them if (transformedFiles) { return transformedFiles.files.map(function (f) { return { name: f.name, slug: generateFileSlug(mainSlug, f.originalName, selectedVariantKey, isInitialVariant), component: f.component }; }); } // Otherwise, create files from original untransformed data var result = []; // Only add main file if it has a fileName if (selectedVariant.fileName && selectedVariant.source) { result.push({ name: selectedVariant.fileName, slug: generateFileSlug(mainSlug, selectedVariant.fileName, selectedVariantKey, isInitialVariant), component: stringOrHastToJsx(selectedVariant.source, shouldHighlight) }); } if (selectedVariant.extraFiles) { Object.entries(selectedVariant.extraFiles).forEach(function (_ref2) { var _ref3 = _slicedToArray(_ref2, 2), fileName = _ref3[0], fileData = _ref3[1]; var source; if (typeof fileData === 'string') { source = fileData; } else if (fileData && _typeof(fileData) === 'object' && 'source' in fileData) { source = fileData.source; } else { return; // Skip invalid entries } if (!source) { return; // Skip null/undefined sources } result.push({ name: fileName, slug: generateFileSlug(mainSlug, fileName, selectedVariantKey, isInitialVariant), component: stringOrHastToJsx(source, shouldHighlight) }); }); } return result; }, [selectedVariant, transformedFiles, mainSlug, selectedVariantKey, variantKeys, initialVariant, shouldHighlight]); // Create a wrapper for selectFileName that handles transformed filenames and URL updates var selectFileName = React.useCallback(function (fileName) { if (!selectedVariant) { return; } var targetFileName = fileName; var fileSlug = ''; // Determine if this is the initial variant var isInitialVariant = initialVariant ? selectedVariantKey === initialVariant : variantKeys.length === 0 || selectedVariantKey === variantKeys[0]; // If we have transformed files, we need to reverse-lookup the original filename if (transformedFiles) { // Check if the fileName is a transformed name - if so, find the original var fileByTransformedName = transformedFiles.files.find(function (f) { return f.name === fileName; }); if (fileByTransformedName) { targetFileName = fileByTransformedName.originalName; fileSlug = generateFileSlug(mainSlug, fileByTransformedName.originalName, selectedVariantKey, isInitialVariant); } else { // Check if the fileName is already an original name var fileByOriginalName = transformedFiles.files.find(function (f) { return f.originalName === fileName; }); if (fileByOriginalName) { targetFileName = fileName; fileSlug = generateFileSlug(mainSlug, fileName, selectedVariantKey, isInitialVariant); } } } else { // No transformed files, generate slug directly fileSlug = generateFileSlug(mainSlug, fileName, selectedVariantKey, isInitialVariant); } // Update the URL hash without adding to history (replaceState) if (typeof window !== 'undefined' && fileSlug) { setHash(fileSlug); // Use the new URL hash hook } markUserInteraction(); // Mark that user has made an explicit selection setSelectedFileNameInternal(targetFileName); }, [selectedVariant, transformedFiles, mainSlug, selectedVariantKey, variantKeys, initialVariant, setHash, markUserInteraction]); return { selectedFileName: selectedFileName, selectedFile: selectedFile, selectedFileComponent: selectedFileComponent, selectedFileLines: selectedFileLines, files: files, selectFileName: selectFileName }; }