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