refactor: rewrite the sidebar to include API docs

This commit is contained in:
Fernando Araoz 2025-01-20 21:25:57 -05:00
parent 479254aa07
commit c26251567c
12 changed files with 215 additions and 274 deletions

View File

@ -1,58 +0,0 @@
---
import type { Hierarchy, Post } from "../../layouts/ApiLayout.astro";
import { splitAndLast } from "../utils";
const hierarchy: Hierarchy = Astro.props.hierarchy;
function postComparison(a: Post, b: Post): number {
const s1 = splitAndLast(a.url);
const s2 = splitAndLast(b.url);
return s1 > s2 ? 0 : 1;
}
function appendSlash(s: string): string {
if (s.endsWith("/")) {
return s;
} else {
return s + "/";
}
}
---
{
Object.entries(hierarchy.children).map(
([folderName, [folderPost, children]]) => (
<>
{folderPost !== null ? (
<a
class="inline-block rounded-2xl w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-2"
href={appendSlash(folderPost.url)}
>
{splitAndLast(folderPost.url)}
</a>
) : (
<div class="mt-6 px-2 py-1 uppercase font-display text-c-text-2 font-medium">
{folderName}
</div>
)}
<div class="pl-2 my-1">
<ul class="border-l border-c-border-1">
<Astro.self hierarchy={children} />
</ul>
</div>
</>
),
)
}
{
hierarchy.posts.sort(postComparison).map((p) => (
<a
class="inline-block rounded-2xl w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-2"
href={appendSlash(p.url)}
>
{splitAndLast(p.url)}
</a>
))
}

View File

@ -1,6 +1,4 @@
--- ---
import { Sun, Moon, Laptop } from "lucide-astro";
const { showSidebarButton = true, version = "latest" } = Astro.props; const { showSidebarButton = true, version = "latest" } = Astro.props;
--- ---

View File

@ -1,20 +1,63 @@
--- ---
import type { PageEntry } from "../layouts/PagesLayout.astro"; import type { AstroFileHierarchy } from "@/layouts/NewDocsLayout.astro";
const entry: PageEntry = Astro.props.entry; const entry: AstroFileHierarchy = Astro.props.entry;
const post_url = entry.url + (entry.url.endsWith("/") ? "" : "/"); const post_url = entry.url + (entry.url.endsWith("/") ? "" : "/");
// this may deal with folders. // this may deal with folders.
// if so, it will turn any `-` into whitespace, // if so, it will turn any `-` into whitespace,
// and remove any leading number // and remove any leading number
const entry_title = entry.title.replaceAll("-", " ").replaceAll(/\d+_/g, ""); const entry_title = entry.title.replaceAll("-", " ").replaceAll(/\d+_/g, "");
const mono_class = entry.frontmatter.mono === true ? "font-mono " : "";
--- ---
{ {
!entry.children && ( entry.children.length > 0 ? (
entry.has_index === true ? (
<>
<li> <li>
<a <a
class="inline-block rounded-lg w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-1" class={
mono_class +
"inline-block rounded-lg w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-1"
}
href={post_url}
>
{entry_title}
</a>
</li>
<ul class="my-1 ml-2 border-l border-c-border-1">
{entry.children!.map((nextEntry) => (
<Astro.self entry={nextEntry} />
))}
</ul>
</>
) : (
<>
<div
class={
mono_class +
"mt-6 px-3 py-1 uppercase font-display text-c-text-2 font-medium"
}
>
{entry_title}
</div>
<ul class="my-1">
{entry.children!.map((nextEntry) => (
<Astro.self entry={nextEntry} />
))}
</ul>
</>
)
) : (
<li>
<a
class={
mono_class +
"inline-block rounded-lg w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-1"
}
href={post_url} href={post_url}
> >
{entry.title} {entry.title}
@ -22,7 +65,9 @@ const entry_title = entry.title.replaceAll("-", " ").replaceAll(/\d+_/g, "");
</li> </li>
) )
} }
{ {
/*
entry.children && ( entry.children && (
<> <>
<div class="mt-6 px-3 py-1 uppercase font-display text-c-text-2 font-medium"> <div class="mt-6 px-3 py-1 uppercase font-display text-c-text-2 font-medium">
@ -36,4 +81,5 @@ const entry_title = entry.title.replaceAll("-", " ").replaceAll(/\d+_/g, "");
</ul> </ul>
</> </>
) )
*/
} }

View File

@ -1,150 +0,0 @@
---
import Navbar from "../components/Navbar.astro";
import BaseLayout from "./BaseLayout.astro";
import TOC from "../components/TOC.astro";
import Sidebar from "../components/ApiLayout/Sidebar.astro";
import { splitAndLast } from "../components/utils";
const { headings } = Astro.props;
export type Post = {
frontmatter: any;
getHeadings: any;
url: string;
file: any;
Content: any;
default: any;
};
type Posts = Array<Readonly<Post>>;
const basePath = "/api/std";
const posts: Posts = await Astro.glob(
"../pages/en/latest/api/std/**/*.{md,mdx}",
);
export type Hierarchy = {
posts: Array<Post>;
children: Record<string, [Post | null, Hierarchy]>;
};
function createHierarchy(posts: Posts): Hierarchy {
const hierarchy: Hierarchy = {
posts: [],
children: {},
};
for (const post of posts) {
const postUrl: string = post.url;
const urlAfterBase = postUrl.split(basePath)[1] ?? "";
// handle / (index)
if (urlAfterBase === "") {
hierarchy.posts.push(post);
continue;
}
const urlSegments = urlAfterBase.split("/").slice(1);
// top level urls
if (urlSegments.length === 1) {
hierarchy.posts.push(post);
continue;
}
// folders
const folders = urlSegments.slice(0, -1);
// traverse the hierarchy until the neccesary hierarchy is found
let currentHierarchy = hierarchy;
for (const folderName of folders) {
// check if folder exists, create otherwise
if (!hierarchy.children[folderName]) {
// create if doesnt exist
hierarchy.children[folderName] = [
null,
{
posts: [],
children: {},
},
];
}
const [_, childrenHierarchy] = hierarchy.children[folderName]!;
currentHierarchy = childrenHierarchy;
}
// add the page
currentHierarchy.posts.push(post);
}
return hierarchy;
}
function normalizeHierarchy(h: Hierarchy): Hierarchy {
let posts = h.posts;
for (const folderName in h.children) {
// search if there is a post with the same name
// as the folder
const postIdx = h.posts.findIndex(
(post) => splitAndLast(post.url) === folderName,
);
if (postIdx !== -1) {
const post = h.posts[postIdx]!;
h.children[folderName]![0] = post;
posts.splice(postIdx, 1);
}
}
// do the same to all children
// TODO
return {
children: h.children,
posts,
};
}
const hierarchy = normalizeHierarchy(createHierarchy(posts));
---
<BaseLayout>
<Navbar />
<div class="lg:grid lg:grid-cols-[14rem_auto_12rem] lg:container mx-auto">
<div
id="sidebar"
class="pt-12 h-screen lg:sticky top-0 fixed z-10 bg-c-bg w-60 lg:w-auto border-r-2 lg:border-0
border-c-border-1 -translate-x-64 lg:translate-x-0 transition-transform"
>
<nav class="py-4 pr-2 overflow-x-scroll h-[calc(100vh-3rem)]">
<Sidebar hierarchy={hierarchy} />
</nav>
</div>
<main
class="py-[3.5rem] lg:pl-12 lg:pr-4 markdown min-w-0 small-container mx-auto"
>
<slot />
<div class="h-32"></div>
</main>
<div
class="lg:pt-12 hidden lg:block pt-4 max-h-screen overflow-x-scroll sticky top-0"
>
<nav class="rounded-md lg:mt-10">
<h2 class="font-display font-medium pb-2 text-c-text-2">
On this page
</h2>
<TOC headings={headings} />
</nav>
</div>
</div>
<script>
import { sidebarHighlight } from "./utils";
// Highlight the current url of the sidebar
document.addEventListener("DOMContentLoaded", sidebarHighlight);
</script>
</BaseLayout>

View File

@ -15,13 +15,46 @@ export interface AstroFile {
__usesAstroImage: boolean; __usesAstroImage: boolean;
url: string; url: string;
file: string; file: string;
relative_file: string;
} }
export interface AstroFileP {
frontmatter: Frontmatter;
__usesAstroImage: boolean;
/**
* Full URL in the filesystem's `pages` folder.
* E.g.: `/en/latest/api/std/Array`
*/
url: string;
/**
* Full URL in the filesystem
* E.g.: `/home/user/project/src/pages/en/latest/api/std/Array`
*/
file: string;
/**
* Title of the page, as found in the frontmatter
* E.g.: `Array`
*/
title: string;
/**
* URL of the file, relative to a `base_url`.
* E.g.: if base_url=`/en/latest/api`, then path=`/std/Array/index.mdx`
*/
path: string;
/**
* Whether or not this file has a children whose name is index.mdx
*/
has_index: boolean;
}
export type AstroFileHierarchy = AstroFileP & {
children: Array<AstroFileHierarchy>;
};
export interface Frontmatter { export interface Frontmatter {
layout: string; layout: string;
title: string; title: string;
order: number; order: number;
mono?: boolean;
} }
type Props = { type Props = {
@ -41,77 +74,108 @@ const {
version = "latest", version = "latest",
}: Props = Astro.props; }: Props = Astro.props;
const base_len = base_url.length; const posts_2: Array<AstroFileP> = posts
const posts_2 = posts
.map((post) => ({ .map((post) => ({
...post, ...post,
title: post.frontmatter.title, title: post.frontmatter.title,
// this should be a path relative to the base url. path: post.file.split(base_url)[1]!,
// e.g if base_url is `/spec`, then this instead of has_index: false,
// being `/spec/ast/tokens` would be `/ast/tokens`
path: post.url.substring(base_len),
})) }))
.sort((p1, p2) => .sort((p1, p2) =>
p1.frontmatter.order > p2.frontmatter.order ? 1 : -1, p1.frontmatter.order > p2.frontmatter.order ? 1 : -1,
); );
// build a hierarchy of the files function buildFileTree(
const second_level: Record<string, Array<AstroFile>> = { flatPosts: Array<AstroFileP>,
_: [], ): Array<AstroFileHierarchy> {
}; // @ts-ignore
for (const post of posts_2) { const root: AstroFileHierarchy = { path: "", children: [] };
const fragments = post.path.split("/");
if (fragments.length === 3) {
const folder_name = fragments[1]!;
// create if not exists flatPosts.forEach((post) => {
if (second_level[folder_name] === undefined) { // Remove leading/trailing slashes and split path
second_level[folder_name] = []; const parts = post.path.replace(/^\/|\/$/g, "").split("/");
} let current = root;
second_level[folder_name].push(post);
} else {
// add to root folder
second_level["_"]!.push(post);
}
}
// build a folder hierarchy (only 2 levels) // Build path segments
parts.forEach((part, index) => {
const path = "/" + parts.slice(0, index + 1).join("/");
let node = current.children?.find(
(child) => child.path === path,
);
const entries: Array<any> = []; if (!node) {
const levels_keys = Object.keys(second_level).toSorted(); node = {
...post,
// The key `_` contains the top level links. Always insert those path,
const top_level_posts = second_level["_"]!; title:
// sort index === parts.length - 1
const sorted_posts = top_level_posts.toSorted(sort_posts); ? post.title
entries.push(...sorted_posts); : part,
children: [],
for (const levels_key of levels_keys) {
if (levels_key === "_") {
// top level, already inserted
continue;
} else {
const posts = second_level[levels_key]!;
const sorted_posts = posts.toSorted(sort_posts);
const parentEntry = {
path: "",
title: levels_key,
children: sorted_posts,
url: "",
}; };
entries.push(parentEntry); current.children?.push(node);
} }
current = node;
});
});
return root.children || [];
} }
function sort_posts(p1, p2) { function sortFileTree(tree: AstroFileHierarchy[]): AstroFileHierarchy[] {
if (!!p1.frontmatter.order && !!p2.frontmatter.order) { return tree
.map((node) => ({
...node,
children: node.children
? sortFileTree(node.children)
: [],
}))
.sort(sort_posts);
}
function sort_posts(p1: AstroFileP, p2: AstroFileP) {
if (p1.frontmatter.order !== p2.frontmatter.order) {
return p1.frontmatter.order > p2.frontmatter.order ? 1 : -1; return p1.frontmatter.order > p2.frontmatter.order ? 1 : -1;
} else { } else {
return p1.title > p2.title ? 1 : -1; return p1.title > p2.title ? 1 : -1;
} }
} }
function markIndexPages(tree: AstroFileHierarchy[]): AstroFileHierarchy[] {
return tree.map((node) => {
const indexChild = node.children?.find((child) =>
child.path.endsWith("index.mdx"),
);
if (indexChild && node.children) {
// Keep children (except index) and has_index, replace everything else
const { children, ...indexProps } = indexChild;
return {
...indexProps,
children: markIndexPages(
node.children.filter(
(child) =>
!child.path.endsWith(
"index.mdx",
),
),
),
has_index: true,
};
}
return {
...node,
children: node.children
? markIndexPages(node.children)
: [],
};
});
}
const posts_hierarchy = sortFileTree(markIndexPages(buildFileTree(posts_2)));
// FIXME: centralize somewhere else
const versions = ["latest", "v0.0.1"].filter((x) => x !== version); const versions = ["latest", "v0.0.1"].filter((x) => x !== version);
const url_components = base_url.split(version); const url_components = base_url.split(version);
const lc = url_components[0] ?? ""; const lc = url_components[0] ?? "";
@ -186,7 +250,7 @@ const rc = url_components[1] ?? "";
<hr class="my-6" /> <hr class="my-6" />
{ {
entries.map((entry) => ( posts_hierarchy.map((entry) => (
<Sidebar entry={entry} /> <Sidebar entry={entry} />
)) ))
} }

View File

@ -1,5 +1,8 @@
--- ---
layout: "@/layouts/ApiLayout.astro" layout: "../_wrapper.astro"
title: concat
order: 1
mono: true
--- ---
import Code from "@/components/Code.astro"; import Code from "@/components/Code.astro";

View File

@ -1,5 +1,8 @@
--- ---
layout: "@/layouts/ApiLayout.astro" layout: "../_wrapper.astro"
title: fold
order: 1
mono: true
--- ---
import Code from "@/components/Code.astro"; import Code from "@/components/Code.astro";

View File

@ -1,5 +1,8 @@
--- ---
layout: "@/layouts/ApiLayout.astro" layout: "../_wrapper.astro"
title: Array
order: 2
mono: true
--- ---
import TwoColumn from "@/components/TwoColumn.astro"; import TwoColumn from "@/components/TwoColumn.astro";

View File

@ -1,5 +1,8 @@
--- ---
layout: "@/layouts/ApiLayout.astro" layout: "../_wrapper.astro"
title: map
order: 1
mono: true
--- ---
import Code from "@/components/Code.astro"; import Code from "@/components/Code.astro";

View File

@ -0,0 +1,24 @@
---
import NewDocsLayout, { type AstroFile } from "@/layouts/NewDocsLayout.astro";
const { frontmatter, headings } = Astro.props;
// Get all the posts from this dir
const posts = (await Astro.glob(
"./**/*.{md,mdx}",
)) as unknown as Array<AstroFile>;
// The base of every URL under this glob
const base_url = "/en/latest/api/std";
const version = "latest";
---
<NewDocsLayout
base_url={base_url}
frontmatter={frontmatter}
headings={headings}
posts={posts}
version={version}
>
<slot />
</NewDocsLayout>

View File

@ -1,5 +1,7 @@
--- ---
layout: "@/layouts/ApiLayout.astro" layout: "./_wrapper.astro"
title: The THP stdlib
order: 1
--- ---
import TwoColumn from "@/components/TwoColumn.astro"; import TwoColumn from "@/components/TwoColumn.astro";

View File

@ -1,5 +1,8 @@
--- ---
layout: "@/layouts/ApiLayout.astro" layout: "./_wrapper.astro"
title: print
order: 2
mono: true
--- ---
import TwoColumn from "@/components/TwoColumn.astro"; import TwoColumn from "@/components/TwoColumn.astro";