Revision control

Copy as Markdown

Other Tools

/* 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(?<code>[\s\S]*?)```/g;
// $code comes from the <code> 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,
"<Canvas withSource='none'><with-common-styles>\n$<code></with-common-styles></Canvas>"
);
}
/**
* 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\/)(?<name>.*?)(?=\/)/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};` : ""}
<Meta
title="${title}"
parameters={{
previewTabs: {
canvas: { hidden: true },
},
viewMode: "docs",
}}
/>
${parseStoriesFromMarkdown(source)}
${
importPath &&
`
## Args Table
<ArgTypes of={"${componentName}"} />
`
}`;
return mdxSource;
}
module.exports = {
getMDXSource,
getStoryTitle,
};