Adds table of contents. Removes now from the top level of the sidebar.
This commit is contained in:
@@ -12,7 +12,6 @@ import SocialLinks from './SocialLinks.astro';
|
|||||||
<SidebarLink href="/">Home</SidebarLink>
|
<SidebarLink href="/">Home</SidebarLink>
|
||||||
<SidebarLink href="/blog">Blog</SidebarLink>
|
<SidebarLink href="/blog">Blog</SidebarLink>
|
||||||
<SidebarLink href="/about">About</SidebarLink>
|
<SidebarLink href="/about">About</SidebarLink>
|
||||||
<SidebarLink href="/now">Now</SidebarLink>
|
|
||||||
<NotesLinks />
|
<NotesLinks />
|
||||||
</div>
|
</div>
|
||||||
<div class="social-links">
|
<div class="social-links">
|
||||||
|
|||||||
31
src/components/TableOfContents.astro
Normal file
31
src/components/TableOfContents.astro
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import TableOfContentsTree from "./TableOfContentsTree.astro";
|
||||||
|
import type { MarkdownHeading } from "astro";
|
||||||
|
import { makeHeadingsTree, removeEmptyLevels } from "../lib/headings";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
headings: Array<MarkdownHeading>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {headings} = Astro.props;
|
||||||
|
const headingsTree = removeEmptyLevels(makeHeadingsTree(headings));
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="table-of-contents">
|
||||||
|
<TableOfContentsTree headingsTree={headingsTree}></TableOfContentsTree>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.table-of-contents {
|
||||||
|
border: 1px solid var(--color-space-blue-light);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
src/components/TableOfContentsTree.astro
Normal file
15
src/components/TableOfContentsTree.astro
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
headingsTree: HeadingsTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {headingsTree} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{headingsTree.map(node =>
|
||||||
|
Array.isArray(node)
|
||||||
|
? <Astro.self headingsTree={node} />
|
||||||
|
: <li><a href={`#${node.slug}`}>{node.text}</a></li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
---
|
---
|
||||||
|
import type { MarkdownHeading } from "astro";
|
||||||
import type { CollectionEntry } from 'astro:content';
|
import type { CollectionEntry } from 'astro:content';
|
||||||
import RootLayout from '../layouts/RootLayout.astro';
|
import RootLayout from '../layouts/RootLayout.astro';
|
||||||
import FormattedDate from '../components/FormattedDate.astro';
|
import FormattedDate from '../components/FormattedDate.astro';
|
||||||
import NotchedBox from '../components/NotchedBox.astro';
|
import NotchedBox from '../components/NotchedBox.astro';
|
||||||
|
import TableOfContents from '../components/TableOfContents.astro';
|
||||||
import { Image } from 'astro:assets';
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
type Props = CollectionEntry<'blog'>['data'] & { slug: string; };
|
type Props = CollectionEntry<'blog'>['data'] & { slug: string; headings: MarkdownHeading[] };
|
||||||
|
|
||||||
const { slug, title, description, pubDate, updatedDate, heroImage, tags } = Astro.props;
|
const { slug, title, description, pubDate, updatedDate, heroImage, tags, headings } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -77,6 +79,7 @@ const { slug, title, description, pubDate, updatedDate, heroImage, tags } = Astr
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<hr />
|
<hr />
|
||||||
|
<TableOfContents headings={headings} />
|
||||||
<div class="e-content">
|
<div class="e-content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
49
src/lib/headings.ts
Normal file
49
src/lib/headings.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { MarkdownHeading } from "astro";
|
||||||
|
|
||||||
|
export type HeadingsTree = Array<MarkdownHeading | HeadingsTree>;
|
||||||
|
|
||||||
|
// could take a list like [5, 1, 2, 2, 3, 5, 1] and should return
|
||||||
|
// [ [ [ [ [ 5 ] ] ] ], 1, [ 2, 2, [3, [[5]]]], 1]
|
||||||
|
export function makeHeadingsTree(headings: MarkdownHeading[]): HeadingsTree {
|
||||||
|
let index = 0;
|
||||||
|
let currDepth = 1;
|
||||||
|
const treeStack = [[]] as Array<HeadingsTree>;
|
||||||
|
|
||||||
|
while (index < headings.length) {
|
||||||
|
const currHeading = headings[index];
|
||||||
|
|
||||||
|
// If the next heading is at the current depth, push it into the tree
|
||||||
|
if (currHeading.depth == currDepth) {
|
||||||
|
treeStack[treeStack.length - 1].push(currHeading);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the next heading is deeper, go deeper
|
||||||
|
if (currHeading.depth > currDepth) {
|
||||||
|
const nextLevel = [] as HeadingsTree;
|
||||||
|
treeStack[treeStack.length - 1].push(nextLevel);
|
||||||
|
treeStack.push(nextLevel);
|
||||||
|
currDepth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the next heading is shallower, back out
|
||||||
|
if (currHeading.depth < currDepth) {
|
||||||
|
treeStack.pop();
|
||||||
|
currDepth -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return treeStack[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeEmptyLevels(tree: HeadingsTree): HeadingsTree {
|
||||||
|
const newLevel = tree.map((node) =>
|
||||||
|
Array.isArray(node) ? removeEmptyLevels(node) : node,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tree.every(Array.isArray)) {
|
||||||
|
return tree.flatMap((node) => node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
@@ -14,9 +14,9 @@ type Props = CollectionEntry<'blog'>;
|
|||||||
|
|
||||||
const post = Astro.props;
|
const post = Astro.props;
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
const { Content } = await render(post);
|
const { Content, headings } = await render(post);
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogPost slug={slug} {...post.data} >
|
<BlogPost slug={slug} headings={headings} {...post.data} >
|
||||||
<Content />
|
<Content />
|
||||||
</BlogPost>
|
</BlogPost>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import RootLayout from '../../layouts/RootLayout.astro';
|
import RootLayout from '../../layouts/RootLayout.astro';
|
||||||
import NotchedBox from '../../components/NotchedBox.astro';
|
import NotchedBox from '../../components/NotchedBox.astro';
|
||||||
import FormattedDate from '../../components/FormattedDate.astro';
|
import FormattedDate from '../../components/FormattedDate.astro';
|
||||||
|
import TableOfContents from '../../components/TableOfContents.astro';
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||||
import { render } from 'astro:content';
|
import { render } from 'astro:content';
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ export async function getStaticPaths() {
|
|||||||
type Props = CollectionEntry<'notes'>;
|
type Props = CollectionEntry<'notes'>;
|
||||||
|
|
||||||
const note = Astro.props;
|
const note = Astro.props;
|
||||||
const { Content } = await render(note);
|
const { Content, headings } = await render(note);
|
||||||
---
|
---
|
||||||
|
|
||||||
<RootLayout>
|
<RootLayout>
|
||||||
@@ -24,6 +25,7 @@ const { Content } = await render(note);
|
|||||||
<div class="last-updated">
|
<div class="last-updated">
|
||||||
Last updated <FormattedDate date={note.data.updated} />
|
Last updated <FormattedDate date={note.data.updated} />
|
||||||
</div>
|
</div>
|
||||||
|
<TableOfContents headings={headings}/>
|
||||||
<Content />
|
<Content />
|
||||||
</NotchedBox>
|
</NotchedBox>
|
||||||
</RootLayout>
|
</RootLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user