All files / src/components/Markdown Markdown.tsx

0% Statements 0/29
0% Branches 0/17
0% Functions 0/4
0% Lines 0/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147                                                                                                                                                                                                                                                                                                     
import { Box } from "@chakra-ui/react";
import { Assembly } from "@jsii/spec";
import githubSchema from "hast-util-sanitize/lib/github.json";
import { FunctionComponent } from "react";
import ReactMarkdown, {
  PluggableList,
  ReactMarkdownOptions,
} from "react-markdown";
import type { TableCellComponent } from "react-markdown/src/ast-to-react";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import remarkEmoji from "remark-emoji";
import remarkGfm from "remark-gfm";
import { Code } from "./Code";
import { Headings } from "./Headings";
import { Hr } from "./Hr";
import { Img } from "./Img";
import { Ul, Ol, Li } from "./List";
import { Table, Thead, Tbody, Tfoot, Tr, Th, Td, TableCaption } from "./Table";
import testIds from "./testIds";
import { A, Blockquote, Em, P, Pre, Sup } from "./Text";
 
type Components = NonNullable<ReactMarkdownOptions["components"]>;
 
const components: Components = {
  a: A,
  blockquote: Blockquote,
  caption: TableCaption,
  code: Code,
  em: Em,
  h1: Headings,
  h2: Headings,
  h3: Headings,
  h4: Headings,
  h5: Headings,
  h6: Headings,
  hr: Hr,
  img: Img,
  li: Li,
  ol: Ol,
  p: P,
  pre: Pre as Components["pre"],
  sup: Sup,
  table: Table,
  tbody: Tbody,
  td: Td as TableCellComponent, // The react-markdown component has a tighter signature than the one from @chakra-ui
  tfoot: Tfoot,
  th: Th as TableCellComponent, // The react-markdown component has a tighter signature than the one from @chakra-ui
  thead: Thead,
  tr: Tr,
  ul: Ul,
};
 
// see below comment
const ghSchema: typeof githubSchema & { attributes: { span?: any } } = {
  ...githubSchema,
};
 
// jsii-docgen adds these attributes to <span> elements embedded inside
// headings in order to configure custom anchor ids.
// tell rehype not to strip those out.
ghSchema.attributes.span = (ghSchema.attributes.span ?? []).concat([
  "dataHeadingTitle",
  "dataHeadingId",
]);
 
// Note - the default schema for rehypeSanitize is GitHub-style, which is what we need!
const rehypePlugins: PluggableList = [
  [rehypeRaw],
  // ALWAYS keep rehypeSanitize LAST!
  [rehypeSanitize, ghSchema],
];
const remarkPlugins = [remarkGfm, remarkEmoji];
 
const GITHUB_REPO_REGEX =
  /^(?:(?:git@)?github\.com:|(?:https?:\/\/)github\.com\/)([^/]+)\/([^/]+)(?:\.git)?$/;
 
/**
 * Parses out a GitHub repository owner and repo name from the `repository`
 * configuration of a jsii Assembly.
 *
 * @returns the `owner` and `repo` for the configured repository, if it looks
 *          like a GitHub repository URL.
 */
const parseGitHubRepository = ({
  type,
  url,
  directory,
}: Assembly["repository"]) => {
  if (type !== "git") {
    return undefined;
  }
  // git@github.com:<owner>/<repo>.git
  // https://github.com/<owner>/<repo>.git
  const match = GITHUB_REPO_REGEX.exec(url);
  if (match == null) {
    return undefined;
  }
 
  const [, owner, repo] = match;
  return { owner, repo, directory };
};
 
export const Markdown: FunctionComponent<{
  children: string;
  repository: Assembly["repository"];
}> = ({ children, repository }) => {
  const repoConfig = parseGitHubRepository(repository);
 
  const toAbsoluteUri = (githubPrefix: string, githubSuffix = "HEAD") =>
    repoConfig == null
      ? ReactMarkdown.uriTransformer
      : (uri: string) => {
          const url = ReactMarkdown.uriTransformer(uri);
 
          // If this is an anchor or absolute URL, return it.
          const [first] = url;
          if (first === "#" || first === "/") {
            return url;
          }
 
          // If there is a protocol element, then return the URL as-is.
          if (url.includes("://")) {
            return url;
          }
 
          const owner = repoConfig.owner;
          const repo = repoConfig.repo.replace(/\.git$/, "");
          const subdir = repoConfig.directory ? `${repoConfig.directory}/` : "";
          return `https://${githubPrefix}/${owner}/${repo}/${githubSuffix}/${subdir}${url}`;
        };
 
  return (
    <Box data-testid={testIds.container} px={8}>
      <ReactMarkdown
        components={components}
        rehypePlugins={rehypePlugins}
        remarkPlugins={remarkPlugins}
        transformImageUri={toAbsoluteUri("raw.githubusercontent.com")}
        transformLinkUri={toAbsoluteUri("github.com", "blob/HEAD")}
      >
        {children}
      </ReactMarkdown>
    </Box>
  );
};