import { Box, Button, Code, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerOverlay, Flex, Grid, GridProps, Heading, Icon, Img, Link, OrderedList, Table, TableContainer, Td, Text, Th, Thead, Tr, UnorderedList, useDisclosure, } from "@chakra-ui/react" import fm from "front-matter" import { readFileSync } from "fs" import _ from "lodash" import { GetStaticPaths, GetStaticProps, NextPage } from "next" import Head from "next/head" import NextLink from "next/link" import { useRouter } from "next/router" import path from "path" import { MdMenu } from "react-icons/md" import ReactMarkdown from "react-markdown" import rehypeRaw from "rehype-raw" import remarkGfm from "remark-gfm" type FilePath = string type UrlPath = string type Route = { path: FilePath title: string description?: string children?: Route[] } type Manifest = { versions: string[]; routes: Route[] } type NavItem = { title: string; path: UrlPath; children?: NavItem[] } type Nav = NavItem[] const readContentFile = (filePath: string) => { const baseDir = process.cwd() const docsPath = path.join(baseDir, "..", "docs") return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" }) } const removeTrailingSlash = (path: string) => path.replace(/\/+$/, "") const removeMkdExtension = (path: string) => path.replace(/\.md/g, "") const removeIndexFilename = (path: string) => { if (path.endsWith("index")) { path = path.replace("index", "") } return path } const removeREADMEName = (path: string) => { if (path.startsWith("README")) { path = path.replace("README", "") } return path } // transformLinkUri converts the links in the markdown file to // href html links. All index page routes are the directory name, and all // other routes are the filename without the .md extension. // This means all relative links are off by one directory on non-index pages. // // index.md -> ./subdir/file = ./subdir/file // index.md -> ../file-next-to-index = ./file-next-to-index // file.md -> ./subdir/file = ../subdir/file // file.md -> ../file-next-to-file = ../file-next-to-file const transformLinkUriSource = (sourceFile: string) => { return (href = "") => { const isExternal = href.startsWith("http") || href.startsWith("https") if (!isExternal) { // Remove .md form the path href = removeMkdExtension(href) // Add the extra '..' if not an index file. sourceFile = removeMkdExtension(sourceFile) if (!sourceFile.endsWith("index")) { href = "../" + href } // Remove the index path href = removeIndexFilename(href) href = removeREADMEName(href) } return href } } const transformFilePathToUrlPath = (filePath: string) => { // Remove markdown extension let urlPath = removeMkdExtension(filePath) // Remove relative path if (urlPath.startsWith("./")) { urlPath = urlPath.replace("./", "") } // Remove index from the root file urlPath = removeIndexFilename(urlPath) urlPath = removeREADMEName(urlPath) // Remove trailing slash if (urlPath.endsWith("/")) { urlPath = removeTrailingSlash(urlPath) } return urlPath } const mapRoutes = (manifest: Manifest): Record => { const paths: Record = {} const addPaths = (routes: Route[]) => { for (const route of routes) { paths[transformFilePathToUrlPath(route.path)] = route if (route.children) { addPaths(route.children) } } } addPaths(manifest.routes) return paths } let manifest: Manifest | undefined const getManifest = () => { if (manifest) { return manifest } const manifestContent = readContentFile("manifest.json") manifest = JSON.parse(manifestContent) as Manifest return manifest } let navigation: Nav | undefined const getNavigation = (manifest: Manifest): Nav => { if (navigation) { return navigation } const getNavItem = (route: Route, parentPath?: UrlPath): NavItem => { const path = parentPath ? `${parentPath}/${transformFilePathToUrlPath(route.path)}` : transformFilePathToUrlPath(route.path) const navItem: NavItem = { title: route.title, path, } if (route.children) { navItem.children = [] for (const childRoute of route.children) { navItem.children.push(getNavItem(childRoute)) } } return navItem } navigation = [] for (const route of manifest.routes) { navigation.push(getNavItem(route)) } return navigation } const removeHtmlComments = (string: string) => { return string.replace(//g, "") } export const getStaticPaths: GetStaticPaths = () => { const manifest = getManifest() const routes = mapRoutes(manifest) const paths = Object.keys(routes).map((urlPath) => ({ params: { slug: urlPath.split("/") }, })) return { paths, fallback: false, } } export const getStaticProps: GetStaticProps = (context) => { // When it is home page, the slug is undefined because there is no url path // so we make it an empty string to work good with the mapRoutes const { slug = [""] } = context.params as { slug: string[] } const manifest = getManifest() const routes = mapRoutes(manifest) const urlPath = slug.join("/") const route = routes[urlPath] const { body } = fm(readContentFile(route.path)) // Serialize MDX to support custom components const content = removeHtmlComments(body) const navigation = getNavigation(manifest) const version = manifest.versions[0] return { props: { content, navigation, route, version, }, } } const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({ item, nav, }) => { const router = useRouter() let isActive = router.asPath.startsWith(`/${item.path}`) // Special case to handle the home path if (item.path === "") { isActive = router.asPath === "/" // Special case to handle the home path children const homeNav = nav.find((navItem) => navItem.path === "") as NavItem const homeNavPaths = homeNav.children?.map((item) => `/${item.path}/`) ?? [] if (homeNavPaths.includes(router.asPath)) { isActive = true } } return ( {item.title} {isActive && item.children && ( {item.children.map((subItem) => ( ))} )} ) } const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({ nav, version, ...gridProps }) => { return ( Coder logo {nav.map((navItem) => ( ))} ) } const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({ nav, version, }) => { const { isOpen, onOpen, onClose } = useDisclosure() return ( <> Coder logo ) } const slugifyTitle = (title: string) => { return _.kebabCase(title.toLowerCase()) } const getImageUrl = (src: string | undefined) => { if (src === undefined) { return "" } const assetPath = src.split("images/")[1] return `/images/${assetPath}` } const DocsPage: NextPage<{ content: string navigation: Nav route: Route version: string }> = ({ content, navigation, route, version }) => { return ( <> {route.title} {/* Some docs don't have the title */} {route.title} ( {children} ), h2: ({ children }) => ( {children} ), h3: ({ children }) => ( {children} ), img: ({ src }) => ( ), p: ({ children }) => ( {children} ), ul: ({ children }) => ( {children} ), ol: ({ children }) => ( {children} ), a: ({ children, href = "" }) => { const isExternal = href.startsWith("http") || href.startsWith("https") return ( {children} ) }, code: ({ node, ...props }) => ( ), pre: ({ children }) => ( code": { w: "full", p: 4, rounded: "md" } }} mb={2} > {children} ), table: ({ children }) => ( {children}
), thead: ({ children }) => {children}, th: ({ children }) => {children}, td: ({ children }) => {children}, tr: ({ children }) => {children}, }} > {content}
) } export default DocsPage