All files / src/components/Markdown Headings.tsx

0% Statements 0/19
0% Branches 0/14
0% Functions 0/4
0% Lines 0/19

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                                                                                                                                                                                                                                     
import { LinkIcon } from "@chakra-ui/icons";
import { Flex, Heading, As } from "@chakra-ui/react";
import { Children, FunctionComponent, ReactNode } from "react";
import ReactDOMServer from "react-dom/server";
import { useLocation } from "react-router-dom";
import { sanitize } from "../../util/sanitize-anchor";
import { NavLink } from "../NavLink";
 
interface HeadingResolverProps {
  level: number;
  children: ReactNode;
}
 
/**
 * Extracts the string leaves from the provided ReactNode.
 *
 * @param node the node from which string data should be fetched.
 *
 * @returns the visible string content from the node.
 */
const stringContent = (node: ReactNode): string => {
  return Children.toArray(node)
    .reduce((acc: string, child) => {
      if (typeof child === "string") {
        return acc + child;
      }
      if (typeof child === "object" && "props" in child) {
        return acc + stringContent(child.props.children);
      }
      return acc;
    }, "")
    .trim();
};
 
const HeadingLink: FunctionComponent<{
  id: string;
  level: number;
  title: string;
}> = ({ id, level, title }) => {
  const { search } = useLocation();
 
  return (
    <NavLink
      _active={{ visibility: "initial" }}
      _focus={{ visibility: "initial" }}
      alignItems="center"
      data-heading-id={`#${id}`}
      data-heading-level={level}
      data-heading-title={title}
      display="flex"
      id={id}
      lineHeight={1}
      opacity="hidden"
      replace
      // Keep search query (or empty string if no query) in url to avoid breaking submodule navigation
      // E.g: #some-page-element, ?submodule=foo#some-page-element
      to={`${search}#${id}`}
      visibility="hidden"
    >
      <LinkIcon boxSize={4} />
    </NavLink>
  );
};
 
export const Headings: FunctionComponent<HeadingResolverProps> = ({
  level,
  children,
}) => {
  const size: string = ["2xl", "xl", "lg", "md", "sm", "xs"][level - 1];
  const elem = `h${level}` as As<any>;
 
  // Use DOMParser to look for data attribute for link ID
  const parser = new DOMParser();
  const doc = parser.parseFromString(
    ReactDOMServer.renderToStaticMarkup(children as React.ReactElement),
    "text/html"
  );
 
  const dataElement = doc.querySelector(
    "span[data-heading-title][data-heading-id]"
  ) as HTMLElement;
  const title = dataElement?.dataset.headingTitle ?? stringContent(children);
 
  const id = dataElement?.dataset.headingId ?? sanitize(title);
 
  return (
    <Flex
      _hover={{
        "> a": {
          visibility: "initial",
        },
      }}
      align="stretch"
      borderBottom="base"
      justify="space-between"
      mb={4}
      mt={level >= 4 ? "1.5em" : 4}
      px={level >= 4 ? 2 : undefined}
      py={2}
    >
      <Heading
        as={elem}
        color="textPrimary"
        level={level}
        size={size}
        sx={{ "> code": { fontSize: "inherit" } }}
      >
        {children}
      </Heading>
 
      <HeadingLink id={id} level={level} title={title} />
    </Flex>
  );
};