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;
---

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("/") ? "" : "/");
// this may deal with folders.
// if so, it will turn any `-` into whitespace,
// and remove any leading number
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>
<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}
>
{entry.title}
@ -22,7 +65,9 @@ const entry_title = entry.title.replaceAll("-", " ").replaceAll(/\d+_/g, "");
</li>
)
}
{
/*
entry.children && (
<>
<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>
</>
)
*/
}

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;
url: 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 {
layout: string;
title: string;
order: number;
mono?: boolean;
}
type Props = {
@ -41,77 +74,108 @@ const {
version = "latest",
}: Props = Astro.props;
const base_len = base_url.length;
const posts_2 = posts
const posts_2: Array<AstroFileP> = posts
.map((post) => ({
...post,
title: post.frontmatter.title,
// this should be a path relative to the base url.
// e.g if base_url is `/spec`, then this instead of
// being `/spec/ast/tokens` would be `/ast/tokens`
path: post.url.substring(base_len),
path: post.file.split(base_url)[1]!,
has_index: false,
}))
.sort((p1, p2) =>
p1.frontmatter.order > p2.frontmatter.order ? 1 : -1,
);
// build a hierarchy of the files
const second_level: Record<string, Array<AstroFile>> = {
_: [],
function buildFileTree(
flatPosts: Array<AstroFileP>,
): Array<AstroFileHierarchy> {
// @ts-ignore
const root: AstroFileHierarchy = { path: "", children: [] };
flatPosts.forEach((post) => {
// Remove leading/trailing slashes and split path
const parts = post.path.replace(/^\/|\/$/g, "").split("/");
let current = root;
// Build path segments
parts.forEach((part, index) => {
const path = "/" + parts.slice(0, index + 1).join("/");
let node = current.children?.find(
(child) => child.path === path,
);
if (!node) {
node = {
...post,
path,
title:
index === parts.length - 1
? post.title
: part,
children: [],
};
for (const post of posts_2) {
const fragments = post.path.split("/");
if (fragments.length === 3) {
const folder_name = fragments[1]!;
current.children?.push(node);
}
current = node;
});
});
// create if not exists
if (second_level[folder_name] === undefined) {
second_level[folder_name] = [];
}
second_level[folder_name].push(post);
} else {
// add to root folder
second_level["_"]!.push(post);
}
return root.children || [];
}
// build a folder hierarchy (only 2 levels)
const entries: Array<any> = [];
const levels_keys = Object.keys(second_level).toSorted();
// The key `_` contains the top level links. Always insert those
const top_level_posts = second_level["_"]!;
// sort
const sorted_posts = top_level_posts.toSorted(sort_posts);
entries.push(...sorted_posts);
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);
}
function sortFileTree(tree: AstroFileHierarchy[]): AstroFileHierarchy[] {
return tree
.map((node) => ({
...node,
children: node.children
? sortFileTree(node.children)
: [],
}))
.sort(sort_posts);
}
function sort_posts(p1, p2) {
if (!!p1.frontmatter.order && !!p2.frontmatter.order) {
function sort_posts(p1: AstroFileP, p2: AstroFileP) {
if (p1.frontmatter.order !== p2.frontmatter.order) {
return p1.frontmatter.order > p2.frontmatter.order ? 1 : -1;
} else {
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 url_components = base_url.split(version);
const lc = url_components[0] ?? "";
@ -186,7 +250,7 @@ const rc = url_components[1] ?? "";
<hr class="my-6" />
{
entries.map((entry) => (
posts_hierarchy.map((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";

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";

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";

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";

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";

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";