From f7a3bb00a5e99cbfe52249a8e2adfbe232857c44 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 20 Apr 2024 22:16:30 +0100 Subject: [PATCH] add breadcrumb and cleanup docs loader --- documentation/landing.md | 2 +- documentation/libraries/java.md | 2 +- documentation/libraries/javascript.md | 2 +- .../documentation/[[...slug]]/page.tsx | 68 ++++--- src/app/common/documentation.ts | 174 +++++++++++------- src/app/components/ui/breadcrumb.tsx | 90 +++++++++ 6 files changed, 244 insertions(+), 94 deletions(-) create mode 100644 src/app/components/ui/breadcrumb.tsx diff --git a/documentation/landing.md b/documentation/landing.md index 99a0c24..bd930e8 100644 --- a/documentation/landing.md +++ b/documentation/landing.md @@ -1,6 +1,6 @@ --- title: Minecraft Utilities Documentation -description: Welcome to the Minecraft Utilities documentation! Here you can find information on how to use the various features of the API. +summary: Welcome to the Minecraft Utilities documentation! Here you can find information on how to use the various features of the API. --- # Getting Started diff --git a/documentation/libraries/java.md b/documentation/libraries/java.md index 6891e1b..6acf1fb 100644 --- a/documentation/libraries/java.md +++ b/documentation/libraries/java.md @@ -1,6 +1,6 @@ --- title: Java Library -description: The Java library for Minecraft Utilities is a simple way to interact with the API using Java! +summary: The Java library for Minecraft Utilities is a simple way to interact with the API using Java! --- # Java Library diff --git a/documentation/libraries/javascript.md b/documentation/libraries/javascript.md index e1f445f..702931f 100644 --- a/documentation/libraries/javascript.md +++ b/documentation/libraries/javascript.md @@ -1,6 +1,6 @@ --- title: Javascript Library -description: The Javascript library for Minecraft Utilities is a simple way to interact with the API using Javascript! +summary: The Javascript library for Minecraft Utilities is a simple way to interact with the API using Javascript! --- # Javascript Library diff --git a/src/app/(pages)/documentation/[[...slug]]/page.tsx b/src/app/(pages)/documentation/[[...slug]]/page.tsx index 594e4be..732d987 100644 --- a/src/app/(pages)/documentation/[[...slug]]/page.tsx +++ b/src/app/(pages)/documentation/[[...slug]]/page.tsx @@ -1,8 +1,17 @@ -import { getDocumentation } from "@/app/common/documentation"; import { CustomMDX } from "@/app/components/mdx-components"; import { Metadata } from "next"; import { generateEmbed } from "@/app/common/embed"; import { Title } from "@/app/components/title"; +import { getDocContent, getDocsContent, getDocsDirectories, getMetadata } from "@/app/common/documentation"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/app/components/ui/breadcrumb"; +import { capitalizeFirstLetter } from "@/app/common/string-utils"; type DocumentationPageParams = { params: { @@ -11,36 +20,15 @@ type DocumentationPageParams = { }; export async function generateStaticParams() { - let documentationPages = getDocumentation(); + let documentationPages = getDocsContent(); return documentationPages.map(page => ({ slug: [page.slug], })); } -/** - * Gets a documentation page by its slug. - * - * @param slug The slug of the documentation page. - */ -function getPage(slug?: string) { - const documentationPages = getDocumentation(); - let page = documentationPages.find(page => page.slug === slug); - - // Fallback to the landing page - if (!page && !slug) { - page = documentationPages.find(page => page.slug === "landing"); - } - - // We still can't find the page, return undefined - if (!page) { - return undefined; - } - return page; -} - export async function generateMetadata({ params: { slug } }: DocumentationPageParams): Promise { - const page = getPage(slug?.join("/")); + const page = getDocContent(slug); // Fallback to page not found if (!page) { @@ -51,13 +39,13 @@ export async function generateMetadata({ params: { slug } }: DocumentationPagePa } return generateEmbed({ - title: `${page.metadata.title} - Documentation`, - description: `${page.metadata.description}\n\nClick to view this page`, + title: `${page.title} - Documentation`, + description: `${page.summary}\n\nClick to view this page`, }); } export default function Page({ params: { slug } }: DocumentationPageParams) { - const page = getPage(slug?.join("/")); + const page = getDocContent(slug); // Page was not found, show an error page if (!page) { @@ -69,11 +57,35 @@ export default function Page({ params: { slug } }: DocumentationPageParams) { ); } + const slugParts = page.slug.split("/"); + return (
+ {slugParts.length > 1 && ( + + + + Home + + {slugParts.map((slug, index, array) => { + const path = array.slice(0, index + 1).join("/"); + + return ( + <> + + + {capitalizeFirstLetter(slug)} + + + ); + })} + + + )} + {/* The documentation page title and description */}
- + <Title title={page.title} subtitle={page.summary} /> </div> {/* The content of the documentation page */} diff --git a/src/app/common/documentation.ts b/src/app/common/documentation.ts index 7f40195..33f54ce 100644 --- a/src/app/common/documentation.ts +++ b/src/app/common/documentation.ts @@ -1,99 +1,147 @@ -/* eslint-disable */ - import * as fs from "node:fs"; import path from "node:path"; -// @ts-ignore -import read from "read-file"; -type Metadata = { +const docsDir = path.join(process.cwd(), "documentation"); + +/** + * Metadata for documentation content. + */ +type DocsContentMetadata = MDXMetadata & { /** - * The title of the documentation page. + * The title of this content. */ title: string; /** - * The description of the documentation page. + * The date this content was published. */ - description: string; + published: string; + + /** + * The summary of this content. + */ + summary: string; }; /** - * The directory where the documentation files are stored. + * Metadata for an MDX file. */ -const documentationDirectory = path.join(process.cwd(), "documentation"); +type MDXMetadata = { + /** + * The slug of the file, defined once read. + */ + slug: string; + + /** + * The metadata of the file. + */ + metadata: { + [key: string]: string; + }; + + /** + * The content of the file. + */ + content: string; +}; /** - * Gets all the documentation files recursively. - * - * @param dirPath the directory path to search for documentation files. + * The regex to match for metadata. */ -function getDocumentationFiles(dirPath: string): string[] { - let files: string[] = []; - const items = fs.readdirSync(dirPath); +const METADATA_REGEX: RegExp = /---\s*([\s\S]*?)\s*---/; - items.forEach(item => { - const itemPath = path.join(dirPath, item); +/** + * Get the directories in the + * given directory. + */ +export function getDocsDirectories(dir: string) { + const dirs: string[] = [dir]; + const paths = fs.readdirSync(dir); + + for (const item of paths) { + const itemPath = path.join(dir, item); const stat = fs.statSync(itemPath); if (stat.isDirectory()) { - // Recursively traverse directories - files.push(...getDocumentationFiles(itemPath)); - } else if (stat.isFile() && path.extname(item) === ".md") { - // Collect markdown files - files.push(itemPath); + dirs.push(...getDocsDirectories(itemPath)); } - }); - - return files; + } + return dirs; } /** - * Gets the content of a documentation file. + * Get the content to + * display in the docs. + */ +export function getDocsContent() { + const directories = getDocsDirectories(docsDir); + const content: DocsContentMetadata[] = []; + + for (let directory of directories) { + content.push(...getMetadata<DocsContentMetadata>(directory)); + } + + return content; +} + +export function getDocContent(path?: string[]) { + const docs = getDocsContent(); + const slug = path ? path.join("/") : "landing"; + + return docs.find(doc => doc.slug === slug); +} + +/** + * Get the metadata of mdx + * files in the given directory. * - * @param file the file to get the content of. + * @param directory the directory to search */ -function getDocumentationFileContent(file: string) { - return read.sync(file, "utf8"); -} - -/** - * Gets all the documentation pages. - */ -export function getDocumentation() { - const files = getDocumentationFiles(documentationDirectory); - - return files.map(file => { - const { metadata, content } = parseFrontmatter(getDocumentationFileContent(file)); - let slug = path.relative(documentationDirectory, file).replace(/\.(md)$/, ""); - slug = slug.replace(/\\/g, "/"); // Normalize path separators - +export function getMetadata<T extends MDXMetadata>(directory: string): T[] { + const files: string[] = fs.readdirSync(directory).filter((file: string): boolean => { + const extension: string = path.extname(file); // The file extension + return extension === ".md" || extension === ".mdx"; + }); // Read the MDX files + return files.map((file: string): T => { + const filePath: string = path.join(directory, file); // The path of the file return { - metadata, - content, - slug, - }; + ...parseMetadata<T>(fs.readFileSync(filePath, "utf-8")), + slug: filePath + .replace(docsDir, "") + .replace(/\.mdx?$/, "") + .replace(/\\/g, "/") + .substring(1), + }; // Map each file to its metadata }); } /** - * Parses the frontmatter of a file. + * Parse the metadata from + * the given content. * - * @param fileContent the content of the file. + * @param content the content to parse + * @returns the metadata and content + * @template T the type of metadata */ -function parseFrontmatter(fileContent: string) { - let frontmatterRegex = /---\s*([\s\S]*?)\s*---/; - let match = frontmatterRegex.exec(fileContent); - let frontMatterBlock = match![1]; - let content = fileContent.replace(frontmatterRegex, "").trim(); - let frontMatterLines = frontMatterBlock.trim().split("\n"); - let metadata: Partial<Metadata> = {}; +function parseMetadata<T extends MDXMetadata>(content: string): T { + const metadataBlock: string = METADATA_REGEX.exec(content)![1]; // Get the block of metadata + content = content.replace(METADATA_REGEX, "").trim(); // Remove the metadata block from the content + let metadata: Partial<{ + [key: string]: string; + }> = {}; // The metadata to return - frontMatterLines.forEach(line => { - let [key, ...valueArr] = line.split(": "); - let value = valueArr.join(": ").trim(); - value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes - metadata[key.trim() as keyof Metadata] = value; - }); + // Parse the metadata block as a key-value pair + metadataBlock + .trim() // Trim any leading or trailing whitespace + .split("\n") // Get each line + .forEach((line: string): void => { + const split: string[] = line.split(": "); // Split the metadata by the colon + let value: string = split[1].trim(); // The value of the metadata + value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes + metadata[split[0].trim()] = value; // Add the metadata to the object + }); - return { metadata: metadata as Metadata, content }; + // Return the metadata and content. The initial + // slug is empty, and is defined later on. + return { ...metadata, content } as T; } diff --git a/src/app/components/ui/breadcrumb.tsx b/src/app/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..44e88a0 --- /dev/null +++ b/src/app/components/ui/breadcrumb.tsx @@ -0,0 +1,90 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/app/common/utils"; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />); +Breadcrumb.displayName = "Breadcrumb"; + +const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>( + ({ className, ...props }, ref) => ( + <ol + ref={ref} + className={cn( + "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", + className, + )} + {...props} + /> + ), +); +BreadcrumbList.displayName = "BreadcrumbList"; + +const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>( + ({ className, ...props }, ref) => ( + <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} /> + ), +); +BreadcrumbItem.displayName = "BreadcrumbItem"; + +const BreadcrumbLink = React.forwardRef< + HTMLAnchorElement, + React.ComponentPropsWithoutRef<"a"> & { + asChild?: boolean; + } +>(({ asChild, className, ...props }, ref) => { + const Comp = asChild ? Slot : "a"; + + return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />; +}); +BreadcrumbLink.displayName = "BreadcrumbLink"; + +const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>( + ({ className, ...props }, ref) => ( + <span + ref={ref} + role="link" + aria-disabled="true" + aria-current="page" + className={cn("font-normal text-foreground", className)} + {...props} + /> + ), +); +BreadcrumbPage.displayName = "BreadcrumbPage"; + +const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => ( + <li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}> + {children ?? <ChevronRight />} + </li> +); +BreadcrumbSeparator.displayName = "BreadcrumbSeparator"; + +const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => ( + <span + role="presentation" + aria-hidden="true" + className={cn("flex h-9 w-9 items-center justify-center", className)} + {...props} + > + <MoreHorizontal className="h-4 w-4" /> + <span className="sr-only">More</span> + </span> +); +BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"; + +export { + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbPage, + BreadcrumbSeparator, + BreadcrumbEllipsis, +};