diff --git a/components/TreeItem.tsx b/components/TreeItem.tsx index 062aa60..df1f538 100644 --- a/components/TreeItem.tsx +++ b/components/TreeItem.tsx @@ -1,8 +1,8 @@ import Link from "next/link"; import { FC } from "react"; -import { TreeItem } from "../lib/tree"; +import { ITreeItem } from "../lib/tree"; -const TreeNode: FC<{ node: TreeItem; path: string }> = ({ +const TreeNode: FC<{ node: ITreeItem; path: string }> = ({ node: { children, value, current, pretty }, path, }) => { diff --git a/lib/tree.ts b/lib/tree.ts index 722843b..82f71f2 100644 --- a/lib/tree.ts +++ b/lib/tree.ts @@ -1,35 +1,147 @@ -export interface TreeItem { +import fm from "front-matter"; +import { readdir, readFile, stat } from "fs/promises"; +import { load } from "js-yaml"; +import { join, resolve } from "path"; +import { removeExt } from "./files"; + +export interface ITreeItem { value: string; current: boolean; - children: TreeItem[]; + children: ITreeItem[]; weight: number; pretty: string | null; } -export class TreeItemConstructor { +export class TreeItem { value: string; current: boolean; - children: TreeItemConstructor[] = []; + children: TreeItem[] = []; weight: number; pretty: string | null; + contents: string | null = null; + constructor( value = "root", - current = false, pretty: string | null = null, - weight = 0 + weight = 0, + contents: string | null = null, + children: TreeItem[] = [] ) { this.value = value; - this.current = current; + this.current = false; this.pretty = pretty; this.weight = weight; + this.contents = contents; + this.children = children; + } + + async addEntry(dir: string, name: string) { + const contents = (await readFile(resolve(dir, name))).toString(); + + const { + attributes: { title, weight }, + } = fm(contents); + + this.addChild( + new TreeItem( + removeExt(name), + title ? title : null, + weight ? weight : 0 + ).setContents(contents) + ); + } + + async walk(dir: string) { + const dirents = await readdir(resolve(process.cwd(), dir), { + withFileTypes: true, + }); + + for (const dirent of dirents) { + if (dirent.name.startsWith(".")) continue; + + if (dirent.isDirectory()) { + const resolvedDir = resolve(dir, dirent.name); + + this.addChild( + await ( + await new TreeItem(removeExt(dirent.name), null, 0).walk( + resolvedDir + ) + ).readConfigYaml(resolvedDir) + ); + } else { + await this.addEntry(dir, dirent.name); + } + } + + return this; + } + + async readConfigYaml(dir: string) { + try { + const configFile = join(dir, ".config.yaml"); + const config = await stat(configFile); + + if (config.isFile()) { + const { title, weight } = load( + (await readFile(configFile)).toString(), + {} + ) as FrontMatter; + + if (weight) this.weight = weight; + if (title) this.pretty = title; + } + } catch (e) { + console.info("Could not load .config.yaml for ", dir); + console.error(e); + } + + return this; + } + + setContents(contents: string) { + this.contents = contents; + return this; + } + + find(slug: string[], current: boolean = false, i = 0): TreeItem | undefined { + if (slug[i] !== this.value) return; + + for (const child of this.children) { + const match = child.find(slug, current, i + 1); + + if (match !== undefined) return match; + } + + if (this.children.length === 0) return this; + } + + walkCurrents(aim: string[], i = 0) { + this.current = aim[i] === this.value; + + if (this.current) { + for (const child of this.children) { + child.walkCurrents(aim, i + 1); + } + } + } + + copy(): TreeItem { + return new TreeItem( + this.value, + this.pretty, + this.weight, + this.contents, + this.children.map((child) => child.copy()) + ); } - addChild(child: TreeItemConstructor) { + addChild(child: TreeItem) { this.children.push(child); } - plain(): TreeItem { + plain(): ITreeItem { return { value: this.value, current: this.current, @@ -47,7 +159,7 @@ export class TreeItemConstructor { } } -export const findCurrentDir = (node: TreeItem): TreeItem | null => { +export const findCurrentDir = (node: ITreeItem): ITreeItem | null => { if (!node.current) { return null; } diff --git a/lib/trees.ts b/lib/trees.ts new file mode 100644 index 0000000..95c6043 --- /dev/null +++ b/lib/trees.ts @@ -0,0 +1,14 @@ +import { i18n } from "../next-i18next.config.js"; +import { TreeItem } from "./tree"; + +const trees: { + [key: string]: TreeItem; +} = {}; + +for (const locale of i18n.locales) { + trees[locale] = await new TreeItem("root").walk(`_docs/${locale}/`); + + trees[locale].sort(); +} + +export default trees; diff --git a/pages/docs/[[...slug]].tsx b/pages/docs/[[...slug]].tsx index b9b37bf..e63740c 100644 --- a/pages/docs/[[...slug]].tsx +++ b/pages/docs/[[...slug]].tsx @@ -1,17 +1,13 @@ import { serialize } from "next-mdx-remote/serialize"; import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote"; import { ReactElement, useEffect } from "react"; -import { join, resolve } from "path"; +import { resolve } from "path"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { removeExt, walkFiles } from "../../lib/files"; -import { readFile, stat } from "fs/promises"; -import { readdir } from "fs/promises"; import remarkGfm from "remark-gfm"; import TreeNode from "../../components/TreeItem"; -import fm from "front-matter"; import DocWrapper from "../../components/DocWrapper"; -import { findCurrentDir, TreeItem, TreeItemConstructor } from "../../lib/tree"; -import { load } from "js-yaml"; +import { findCurrentDir, ITreeItem } from "../../lib/tree"; import rehypeAutolinkHeadings from "rehype-autolink-headings"; import rehypeSlug from "rehype-slug"; import rehypeHighlight from "rehype-highlight"; @@ -20,6 +16,7 @@ import { useRouter } from "next/router"; import Link from "next/link"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import TranslationInfo from "../../components/TranslationInfo"; +import trees from "../../lib/trees"; export const getStaticPaths: GetStaticPaths = async ({ locales }) => { const paths: GetStaticPathsResult["paths"] = []; @@ -54,67 +51,14 @@ export const getStaticProps: GetStaticProps = async ({ params, locale }) => { "meta", ]); - let path = ["_docs", locale, ...slug].join("/") + ".mdx"; - let isDir: boolean = false; - - const walk = async (node: TreeItemConstructor, dir: string, i = 0) => { - const dirents = await readdir(resolve(process.cwd(), dir), { - withFileTypes: true, - }); - for (const dirent of dirents.filter( - (dirent) => !dirent.name.startsWith(".") - )) { - const current = slug[i] === removeExt(dirent.name) && node.current; - if (dirent.isDirectory()) { - node.addChild( - await walk( - new TreeItemConstructor(removeExt(dirent.name), current, null, 0), - resolve(dir, dirent.name), - i + 1 - ) - ); - - try { - const configFile = join(resolve(dir, dirent.name), ".config.yaml"); - const config = await stat(configFile); - - if (config.isFile()) { - const { title, weight } = load( - (await readFile(configFile)).toString(), - {} - ) as FrontMatter; - if (title) node.children.at(-1)!.pretty = title; - if (weight) node.children.at(-1)!.weight = weight; - } - } catch (_) { } - - if (current && i + 1 == slug.length && node.children.length > 0) { - isDir = true; - } - } else { - const contents = (await readFile(resolve(dir, dirent.name))).toString(); - - const { - attributes: { title, weight }, - } = fm(contents); - node.addChild( - new TreeItemConstructor( - removeExt(dirent.name), - current, - title ? title : null, - weight ? weight : 0 - ) - ); - } - } - return node; - }; + const rootySlug = ["root", ...slug]; + + const tree = trees[locale!].copy(); + tree.walkCurrents(rootySlug); - const tree = ( - await walk(new TreeItemConstructor("root", true), `_docs/${locale}/`) - ).sort(); + const me = tree.find(rootySlug); - if (isDir || slug.length === 0) { + if (slug.length === 0 || me === undefined) { const plain = tree.plain(); return { props: { @@ -126,29 +70,24 @@ export const getStaticProps: GetStaticProps = async ({ params, locale }) => { }; } - const mdxSource = await serialize( - (await readFile(resolve(process.cwd(), path))).toString(), - { - parseFrontmatter: true, - mdxOptions: { - remarkPlugins: [remarkGfm], - rehypePlugins: [ - rehypeSlug, - [ - rehypeAutolinkHeadings, - { - behavior: "wrap", - }, - ], - rehypeHighlight, - ], - }, - } - ); - return { props: { - source: mdxSource, + source: await serialize(me.contents!, { + parseFrontmatter: true, + mdxOptions: { + remarkPlugins: [remarkGfm], + rehypePlugins: [ + rehypeSlug, + [ + rehypeAutolinkHeadings, + { + behavior: "wrap", + }, + ], + rehypeHighlight, + ], + }, + }), tree: tree.plain(), ...translations, }, @@ -157,18 +96,19 @@ export const getStaticProps: GetStaticProps = async ({ params, locale }) => { const DocPage: NextPageWithLayout<{ source: MDXRemoteSerializeResult | null; - tree: TreeItem; - dir: TreeItem | null; + tree: ITreeItem; + dir: ITreeItem | null; }> = ({ source, tree, dir }) => { const { - query: { slug }, push + query: { slug }, + push, } = useRouter(); useEffect(() => { if (slug === undefined) { - push("/docs/crystal-linux/getting-started") + push("/docs/crystal-linux/getting-started"); } - }, [push, slug]) + }, [push, slug]); return (
@@ -182,13 +122,16 @@ const DocPage: NextPageWithLayout<{ {dir.pretty !== null &&

{dir.pretty}

} ) : ( diff --git a/tsconfig.json b/tsconfig.json index dfee693..8626b70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,