/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint-env node */ const path = require("path"); const fs = require("fs"); const projectRoot = path.resolve(__dirname, "../../../../"); /** * Takes a file path and returns a string to use as the story title, capitalized * and split into multiple words. The file name gets transformed into the story * name, which will be visible in the Storybook sidebar. For example, either: * * /stories/hello-world.stories.md or /stories/helloWorld.md * * will result in a story named "Hello World". * * @param {string} filePath - path of the file being processed. * @returns {string} The title of the story. */ function getTitleFromPath(filePath) { let fileName = path.basename(filePath, ".stories.md"); if (fileName != "README") { try { let relatedFilePath = path.resolve( "../../../", filePath.replace(".md", ".mjs") ); let relatedFile = fs.readFileSync(relatedFilePath).toString(); let relatedTitle = relatedFile.match(/title: "(.*)"/)[1]; if (relatedTitle) { return relatedTitle + "/README"; } } catch {} } return separateWords(fileName); } /** * Splits a string into multiple capitalized words e.g. hello-world, helloWorld, * and hello.world all become "Hello World." * @param {string} str - String in any case. * @returns {string} The string split into multiple words. */ function separateWords(str) { return ( str .match(/[A-Z]?[a-z0-9]+/g) ?.map(text => text[0].toUpperCase() + text.substring(1)) .join(" ") || str ); } /** * Enables rendering code in our markdown docs by parsing the source for * annotated code blocks and replacing them with Storybook's Canvas component. * @param {string} source - Stringified markdown source code. * @returns {string} Source with code blocks replaced by Canvas components. */ function parseStoriesFromMarkdown(source) { let storiesRegex = /```(?:js|html) story\n(?[\s\S]*?)```/g; // $code comes from the capture group in the regex above. It consists // of any code in between backticks and gets run when used in a Canvas component. return source.replace( storiesRegex, "\n$" ); } /** * Finds the name of the component for files in toolkit widgets. * @param {string} resourcePath - Path to the file being processed. * @returns The component name e.g. "moz-toggle" */ function getComponentName(resourcePath) { let componentName = ""; if (resourcePath.includes("toolkit/content/widgets")) { let storyNameRegex = /(?<=\/widgets\/)(?.*?)(?=\/)/g; componentName = storyNameRegex.exec(resourcePath)?.groups?.name; } return componentName; } /** * Figures out where a markdown based story should live in Storybook i.e. * whether it belongs under "Docs" or "UI Widgets" as well as what name to * display in the sidebar. * @param {string} resourcePath - Path to the file being processed. * @returns {string} The title of the story. */ function getStoryTitle(resourcePath) { // Currently we sort docs only stories under "Docs" by default. let storyPath = "Docs"; let relativePath = path .relative(projectRoot, resourcePath) .replaceAll(path.sep, "/"); let componentName = getComponentName(relativePath); if (componentName) { // Get the common name for a component e.g. Toggle for moz-toggle storyPath = "UI Widgets/" + separateWords(componentName).replace(/^Moz/g, ""); } let storyTitle = getTitleFromPath(relativePath); let title = storyTitle.includes("/") ? storyTitle : `${storyPath}/${storyTitle}`; return title; } /** * Figures out the path to import a component for cases where we have * interactive examples in the docs that require the component to have been * loaded. This wasn't necessary prior to Storybook V7 since everything was * loaded up front; now stories are loaded on demand. * @param {string} resourcePath - Path to the file being processed. * @returns Path used to import a component into a story. */ function getImportPath(resourcePath) { // We need to normalize the path for this logic to work cross-platform. let normalizedPath = resourcePath.split(path.sep).join("/"); // Limiting this to toolkit widgets for now since we don't have any // interactive examples in other docs stories. if (!normalizedPath.includes("toolkit/content/widgets")) { return ""; } let componentName = getComponentName(normalizedPath); let fileExtension = ""; if (componentName) { let mjsPath = normalizedPath.replace( "README.stories.md", `${componentName}.mjs` ); let jsPath = normalizedPath.replace( "README.stories.md", `${componentName}.js` ); if (fs.existsSync(mjsPath)) { fileExtension = "mjs"; } else if (fs.existsSync(jsPath)) { fileExtension = "js"; } else { return ""; } } return `"toolkit-widgets/${componentName}/${componentName}.${fileExtension}"`; } /** * Takes markdown and re-writes it to MDX. Conditionally includes a table of * arguments when we're documenting a component. * @param {string} source - The markdown source to rewrite to MDX. * @param {string} title - The title of the story. * @param {string} resourcePath - Path to the file being processed. * @returns The markdown source converted to MDX. */ function getMDXSource(source, title, resourcePath = "") { let importPath = getImportPath(resourcePath); let componentName = getComponentName(resourcePath); // Unfortunately the indentation/spacing here seems to be important for the // MDX parser to know what to do in the next step of the Webpack process. let mdxSource = ` import { Meta, Canvas, ArgTypes } from "@storybook/addon-docs"; ${importPath ? `import ${importPath};` : ""} ${parseStoriesFromMarkdown(source)} ${ importPath && ` ## Args Table ` }`; return mdxSource; } module.exports = { getMDXSource, getStoryTitle, };