summaryrefslogtreecommitdiffstats
path: root/browser/components/storybook/.storybook/markdown-story-utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/storybook/.storybook/markdown-story-utils.js')
-rw-r--r--browser/components/storybook/.storybook/markdown-story-utils.js197
1 files changed, 197 insertions, 0 deletions
diff --git a/browser/components/storybook/.storybook/markdown-story-utils.js b/browser/components/storybook/.storybook/markdown-story-utils.js
new file mode 100644
index 0000000000..1cc78164ad
--- /dev/null
+++ b/browser/components/storybook/.storybook/markdown-story-utils.js
@@ -0,0 +1,197 @@
+/* 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) {
+ // Limiting this to toolkit widgets for now since we don't have any
+ // interactive examples in other docs stories.
+ if (!resourcePath.includes("toolkit/content/widgets")) {
+ return "";
+ }
+ let componentName = getComponentName(resourcePath);
+ let fileExtension = "";
+ if (componentName) {
+ let mjsPath = resourcePath.replace(
+ "README.stories.md",
+ `${componentName}.mjs`
+ );
+ let jsPath = resourcePath.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,
+};