background

We want to be able to write component library documentation like markdown and automatically generate component library documentation sites.

Simple effect demonstration

First, configure markdown-site-loader

{
  test: /\.md$/,
  use: [
    {
      loader: "markdown-site-loader".options: {
        codeOutputPath: CODE_OUTPUT_PATH,
      },
    },
  ],
},
Copy the code

Where codeOutputPath is the output path of the extracted code.

It is automatically output by markdown-site-loader and contains all code snippets from Markdown as well as the index.json configuration file, which you can use to find the corresponding code snippets.

. ├ ─ ─ 271100111991154798117116116111110471151169711411646109100. The TSX ├ ─ ─ 27147113117105991078311697114116471151169711411646109100. The TSX ├ ─ ─ 91100111991154798117116116111110471151169711411646109100. The TSX ├ ─ ─ 9111547113117105991078311697114116471001011159946109100. The TSX ├ ─ ─ 9147113117105991078311697114116471151169711411646109100. The TSX ├ ─ ─ 919947100111991154798117116116111110479811611046109100. The TSX └ ─ ─ index. JsonCopy the code

Second, write config.ts

This is actually the last step, writing a config.ts file. Specify the site navigation title, routing path, and markdown file path in the file. Markdown-site automatically does the markdown file lookup, parsing, and rendering logic for you. Just focus on writing your document content using Markdown!

export default[{title: "Introduction".children: [{title: "Quick start".path: "/".MDPath: "./docs/quickStart/start.md"}, {title: "Description".path: "/quickStart/desc".MDPath: "./docs/quickStart/desc.md",},],}, {title: "Button".children: [{title: "Start".path: "/button/start".MDPath: "./docs/button/start.md"}, {title: "Button use".path: "/button/btn".MDPath: "./docs/button/btn.md",},],},];Copy the code

Such as/docs/quickStart/start. The md content is as follows:

~~~code import React from "React "; import { Dialog, Button } from "zent"; const { openDialog } = Dialog; const Demo: React.FC = () => { const open = () => { openDialog({ title: "title1100", children: <div>Dialog</div>, }); }; Return (<Button type="primary" onClick={open}>); }; export default Demo; ~ ~ ~Copy the code

The rendering effect in the website is as follows (the style can be customized) :

Technology selection

  • Unified lets you process Markdown text using syntax trees
  • React – markDown renders the markdown component as react
  • React.lazy Loads the component file asynchronously

Introduction of the principle

Start by writing a WebPack Loader using Unified to extract the code from MarkDown. A code snippet like the following is extracted:

~~~code import React from "react"; import { Button} from "zent"; Const Demo: react. FC = () => {return <Button type="primary"> </Button>; }; export default Demo; ~ ~ ~Copy the code

Then use the React -markdown to render the markdown into the React component (that is, HTML) and render it on the site.

The last step is the most critical, since react-Markdown determines the language of the text during rendering.

Such as

~~~code Some code... ~ ~ ~Copy the code

The language of the text is code.

Therefore, when judging language = code, in addition to displaying the code normally, we can use the previously extracted code and use react. lazy to render the React component written by the code. In this way, the document website can display the sample code, also can display the UI and interactive effect of the sample code.

Implementation,

The project consists of three parts.

Markdown-site-front is responsible for the presentation of a website, such as navigation, layout, document content rendering, etc.

Markdown-site-loader is used to compile markdown files, extract codes in Markdown, and generate code configuration files.

Markdown-site-shared is nothing special to mention. It stores some common constants

Three parts (packages) are managed using LerNA.

├─ Markdown site-front ├─ Markdown site-loader ├─ Markdown site-sharedCopy the code

markdown-site-loader

Get markdownParser

// markdownParser
import unified from "unified";
import remarkParse from "remark-parse";

export default unified().use(remarkParse).freeze();
Copy the code

Get the Markdown syntax tree

module.exports = function (source: string) {
  const ast = markdownParser.parse(source);

  return `export default The ${JSON.stringify(source)}; `;
};
Copy the code

Extract the code and write it to the codeOutputPath directory of the Markdown-site-loader Option configuration. To find the corresponding code file, you also need to generate a configuration file (writeCodeConfig).

const codeConfig: ICodeConfigItem[] = [];

ast.children.forEach((child: IASTChild) = > {
  const { type, value } = child;

  // CODE_IDENTIFIER (default) = "code"
  if (type === CODE_IDENTIFIER) {
    const position = child.position.start;
    const codeFileName = genCodeFileName(resourcePath, position);

    writeCodeFile(codeOutputPath, codeFileName, value);

    codeConfig.push({
      position,
      resourcePath,
      codePath: `${codeFileName}.tsx`}); }}); writeCodeConfig(codeOutputPath,JSON.stringify(codeConfig));
Copy the code

GenCodeFileName is the unique name used to generate the code file. Generated using positionStr + pathCharCodeStr.

import { IASTPosition } from "markdown-site-shared";

const PATH_LIMIT = 20;

const genCodeFileName = (resourcePath: string, position: IASTPosition) = > {
  const positionStr = `${position.line}${position.column}`;

  const pathList = resourcePath.split("");
  const len = pathList.length;
  const pathCharCodeStr = pathList
    .slice(len - PATH_LIMIT, len)
    .map((char) = > char.charCodeAt(0))
    .join("");

  return positionStr + pathCharCodeStr;
};

export default genCodeFileName;
Copy the code

markdown-site-front

The core logic of markdown-site-front is in markdown.tsx (consider this later).

First it retrieves the contents of markDown based on the markDown file path passed in.

/** * 1. Dynamic import paths do not support variable transmission, so use template string * 2. Loader path matching requires suffix, so end with.md ** /
const getMarkdown = (path) = > {
  const pathWithoutSuffix = path.replace(/\.md$/."");

  return new Promise<string> ((resolve) = > {
    import(`${pathWithoutSuffix}.md`).then((module) = > resolve(module.default));
  });
};

const Markdown: React.FC<IMarkdownProps> = ({ path }) = > {
  const [content, setContent] = useState("");
  useEffect(() = > {
    path && getMarkdown(path).then((content) = >setContent(content)); } []);if(! content) {return null; }};export default Markdown;
Copy the code

Then use the ability provided by React-MarkDown to determine when language = “code”, in addition to highlighting the code normally

<SyntaxHighlighter
  children={String(children).replace(/\n$/, "")}
  style={vscDarkPlus}
  language="javascript"
  PreTag="div"
/>
Copy the code

Get the corresponding code according to the codeConfig (getCodeConf), and use the React. Lazy to show the React component represented by the code. In this way, the document has both code examples, and the corresponding UI and interactive functions!

At this point, all the key logic of markdown-site is covered.

import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import codeConfig from "./codeDist/index.json";
import { CODE_IDENTIFIER, IASTPosition } from "markdown-site-shared";

const getCodeConf = (path: string, position: IASTPosition) = > {
  const comparePath = path
    .split("/")
    .filter((item) = > !$/ / ^ \. +.test(item))
    .join("/");

  const conf = codeConfig.find((item) = > {
    return (
      item.resourcePath.endsWith(comparePath) &&
      item.position.offset === position.offset
    );
  });

  return conf;
};

const Markdown: React.FC<IMarkdownProps> = ({ path }) = > {
  return (
    <ReactMarkdown
      children={content}
      components={{
        code({ className.children.. element{})if (className= = = `language-The ${CODE_IDENTIFIER{} `)const position = element.node.position? .start as IASTPosition;

            const conf = getCodeConf(path, position);

            const Code = React.lazy(() = >import(`./codeDist/${conf? .codePath}`) ); return (<>
                <React.Suspense fallback={<div>loading...</div>} ><Code />
                </React.Suspense>
                <SyntaxHighlighter
                  children={String(children).replace(/\n$/, "")}
                  style={vscDarkPlus}
                  language="javascript"
                  PreTag="div"
                />
              </>
            );
          } else {
            return <code className={className}>{children}</code>; }},}} />); };export default Markdown;
Copy the code

Source code address

  • markdown-site