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 { ReactNode } from "react"; 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 = (titleSource: ReactNode) => { if (Array.isArray(titleSource) && typeof titleSource[0] === "string") { return _.kebabCase(titleSource[0].toLowerCase()); } return undefined; }; 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;