.ts
TypeScript
(application/typescript)
// 3rd-party
import React, { useMemo, VFC } from "react";
import { compiler as markdownToJsxCompiler } from "markdown-to-jsx";
import styled, { css } from "styled-components";
// app
import type { AppThemeScheme, WithThemeSchemeProp } from "../types";
import { Card } from "./Card.styled";
import { NamedColors } from "../utils/style";
// app islands
import Code, { getThemedCodeCss, StyledCodeTag } from "../islands/Code";

type MarkdownToJsxProps = {
  themeScheme: AppThemeScheme;
  markdown: string;
};

export const MarkdownToJsx: VFC<MarkdownToJsxProps> = ({
  themeScheme,
  markdown,
}) => {
  const MarkdownElements = useMemo(
    () => () =>
      markdownToJsxCompiler(markdown, {
        wrapper: ({ children }) => <>{children}</>,
        forceBlock: true,
        overrides: {
          a: ({ children, href, title, ...props }) => (
            <StyledAnchor
              {...props}
              href={href}
              themeScheme={themeScheme}
              title={title}
              target={href.startsWith("http") ? "_blank" : undefined}
              rel={
                href.startsWith("http")
                  ? "noopener noreferer noreferrer"
                  : undefined
              }
            >
              {children}
            </StyledAnchor>
          ),
          p: ({ children }) => (
            <StyledParagraph themeScheme={themeScheme}>
              {children}
            </StyledParagraph>
          ),
          blockquote: ({ children, ...props }) => (
            <StyledBlockquoteCard {...props} themeScheme={themeScheme}>
              {children}
            </StyledBlockquoteCard>
          ),
          pre: ({ children, ...props }) => {
            if ("type" in children && children["type"] === "code") {
              const { className, children: code } = children["props"];
              const language = (className || "lang-plaintext").replace(
                "lang-",
                ""
              );
              return (
                <Code
                  code={code}
                  language={language}
                  themeScheme={themeScheme}
                />
              );
            }
            return (
              <StyledCodeTag {...props} themeScheme={themeScheme}>
                {children}
              </StyledCodeTag>
            );
          },
          ul: ({ children, ...props }) => (
            <ul style={{ paddingLeft: 24 }} {...props}>
              {children}
            </ul>
          ),
          li: ({ children, ...props }) => (
            <li style={{ marginTop: 4 }} {...props}>
              {children}
            </li>
          ),
          ImageWithTheme: ({
            alt,
            lightSrc,
            darkSrc,
            minHeight = undefined,
          }: {
            alt: string;
            lightSrc: string;
            darkSrc: string;
            minHeight?: number;
          }) => (
            <figure>
              <img
                alt={alt}
                src={
                  themeScheme === "light"
                    ? JSON.parse(darkSrc)
                    : JSON.parse(lightSrc)
                }
                style={{
                  width: "100%",
                  height: "auto",
                  minHeight: `${minHeight}px`,
                }}
              />
              <figcaption
                style={{
                  textAlign: "center",
                  opacity: 0.67,
                  fontSize: 12,
                  fontStyle: "italic",
                }}
              >
                {JSON.parse(alt)}
              </figcaption>
            </figure>
          ),
        },
      }),
    [markdown]
  );

  return (
    <StyledMarkdownContainer className={"md"} themeScheme={themeScheme}>
      {getThemedCodeCss(themeScheme)}
      <MarkdownElements />
    </StyledMarkdownContainer>
  );
};

const StyledMarkdownContainer = styled.div<WithThemeSchemeProp>`
  width: 100%;
  max-width: 100%;

  font-size: 18px;
  line-height: 26px;

  & > blockquote:first-child,
  & > div:first-child,
  & > pre:first-child,
  & > code:first-child,
  & > body:first-child,
  & > dd:first-child,
  & > dl:first-child,
  & > figure:first-child,
  & > h1:first-child,
  & > h2:first-child,
  & > h3:first-child,
  & > h4:first-child,
  & > h5:first-child,
  & > p:first-child {
    margin-top: 0 !important;
  }

  & code {
    min-height: 20px;
    padding: 2px 4px;

    font-size: 16px;
    white-space: break-spaces;
    border-radius: 4px !important;

    ${({ themeScheme }) => css`
      background-color: ${NamedColors.CARD[themeScheme]} !important;
      border: 1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]} !important;
    `};
  }
`;

const StyledAnchor = styled.a<WithThemeSchemeProp>`
  ${({ themeScheme }) => css`
    color: ${NamedColors.TEXT_LINK[themeScheme]};
  `};

  font-weight: bold;
  text-decoration: none;

  &:hover {
    text-decoration: underline;
  }
`;

const StyledParagraph = styled.p<WithThemeSchemeProp>`
  margin-top: 16px;

  ${({ themeScheme }) => css`
    color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
  `};
`;

const StyledBlockquoteCard = styled(Card)<WithThemeSchemeProp>`
  & > p {
    margin: 0;
    margin-top: 16px;
  }

  & > div,
  & > blockquote {
    margin-top: 8px;
  }

  & > pre {
    margin: 0;
    margin-top: 8px;
  }

  & > blockquote:first-child,
  & > div:first-child,
  & > pre:first-child,
  & > code:first-child,
  & > body:first-child,
  & > dd:first-child,
  & > dl:first-child,
  & > figure:first-child,
  & > h1:first-child,
  & > h2:first-child,
  & > h3:first-child,
  & > h4:first-child,
  & > h5:first-child,
  & > p:first-child {
    margin-top: 0 !important;
  }
`;

GitFOSS
.ts
TypeScript
(application/typescript)
// 3rd-party
import React, { useMemo, VFC } from "react";
import { compiler as markdownToJsxCompiler } from "markdown-to-jsx";
import styled, { css } from "styled-components";
// app
import type { AppThemeScheme, WithThemeSchemeProp } from "../types";
import { Card } from "./Card.styled";
import { NamedColors } from "../utils/style";
// app islands
import Code, { getThemedCodeCss, StyledCodeTag } from "../islands/Code";

type MarkdownToJsxProps = {
  themeScheme: AppThemeScheme;
  markdown: string;
};

export const MarkdownToJsx: VFC<MarkdownToJsxProps> = ({
  themeScheme,
  markdown,
}) => {
  const MarkdownElements = useMemo(
    () => () =>
      markdownToJsxCompiler(markdown, {
        wrapper: ({ children }) => <>{children}</>,
        forceBlock: true,
        overrides: {
          a: ({ children, href, title, ...props }) => (
            <StyledAnchor
              {...props}
              href={href}
              themeScheme={themeScheme}
              title={title}
              target={href.startsWith("http") ? "_blank" : undefined}
              rel={
                href.startsWith("http")
                  ? "noopener noreferer noreferrer"
                  : undefined
              }
            >
              {children}
            </StyledAnchor>
          ),
          p: ({ children }) => (
            <StyledParagraph themeScheme={themeScheme}>
              {children}
            </StyledParagraph>
          ),
          blockquote: ({ children, ...props }) => (
            <StyledBlockquoteCard {...props} themeScheme={themeScheme}>
              {children}
            </StyledBlockquoteCard>
          ),
          pre: ({ children, ...props }) => {
            if ("type" in children && children["type"] === "code") {
              const { className, children: code } = children["props"];
              const language = (className || "lang-plaintext").replace(
                "lang-",
                ""
              );
              return (
                <Code
                  code={code}
                  language={language}
                  themeScheme={themeScheme}
                />
              );
            }
            return (
              <StyledCodeTag {...props} themeScheme={themeScheme}>
                {children}
              </StyledCodeTag>
            );
          },
          ul: ({ children, ...props }) => (
            <ul style={{ paddingLeft: 24 }} {...props}>
              {children}
            </ul>
          ),
          li: ({ children, ...props }) => (
            <li style={{ marginTop: 4 }} {...props}>
              {children}
            </li>
          ),
          ImageWithTheme: ({
            alt,
            lightSrc,
            darkSrc,
            minHeight = undefined,
          }: {
            alt: string;
            lightSrc: string;
            darkSrc: string;
            minHeight?: number;
          }) => (
            <figure>
              <img
                alt={alt}
                src={
                  themeScheme === "light"
                    ? JSON.parse(darkSrc)
                    : JSON.parse(lightSrc)
                }
                style={{
                  width: "100%",
                  height: "auto",
                  minHeight: `${minHeight}px`,
                }}
              />
              <figcaption
                style={{
                  textAlign: "center",
                  opacity: 0.67,
                  fontSize: 12,
                  fontStyle: "italic",
                }}
              >
                {JSON.parse(alt)}
              </figcaption>
            </figure>
          ),
        },
      }),
    [markdown]
  );

  return (
    <StyledMarkdownContainer className={"md"} themeScheme={themeScheme}>
      {getThemedCodeCss(themeScheme)}
      <MarkdownElements />
    </StyledMarkdownContainer>
  );
};

const StyledMarkdownContainer = styled.div<WithThemeSchemeProp>`
  width: 100%;
  max-width: 100%;

  font-size: 18px;
  line-height: 26px;

  & > blockquote:first-child,
  & > div:first-child,
  & > pre:first-child,
  & > code:first-child,
  & > body:first-child,
  & > dd:first-child,
  & > dl:first-child,
  & > figure:first-child,
  & > h1:first-child,
  & > h2:first-child,
  & > h3:first-child,
  & > h4:first-child,
  & > h5:first-child,
  & > p:first-child {
    margin-top: 0 !important;
  }

  & code {
    min-height: 20px;
    padding: 2px 4px;

    font-size: 16px;
    white-space: break-spaces;
    border-radius: 4px !important;

    ${({ themeScheme }) => css`
      background-color: ${NamedColors.CARD[themeScheme]} !important;
      border: 1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]} !important;
    `};
  }
`;

const StyledAnchor = styled.a<WithThemeSchemeProp>`
  ${({ themeScheme }) => css`
    color: ${NamedColors.TEXT_LINK[themeScheme]};
  `};

  font-weight: bold;
  text-decoration: none;

  &:hover {
    text-decoration: underline;
  }
`;

const StyledParagraph = styled.p<WithThemeSchemeProp>`
  margin-top: 16px;

  ${({ themeScheme }) => css`
    color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
  `};
`;

const StyledBlockquoteCard = styled(Card)<WithThemeSchemeProp>`
  & > p {
    margin: 0;
    margin-top: 16px;
  }

  & > div,
  & > blockquote {
    margin-top: 8px;
  }

  & > pre {
    margin: 0;
    margin-top: 8px;
  }

  & > blockquote:first-child,
  & > div:first-child,
  & > pre:first-child,
  & > code:first-child,
  & > body:first-child,
  & > dd:first-child,
  & > dl:first-child,
  & > figure:first-child,
  & > h1:first-child,
  & > h2:first-child,
  & > h3:first-child,
  & > h4:first-child,
  & > h5:first-child,
  & > p:first-child {
    margin-top: 0 !important;
  }
`;