Add ToC to blog posts
This commit is contained in:
parent
008bfa4d94
commit
58d3d01cbe
7 changed files with 243 additions and 8 deletions
|
|
@ -15,7 +15,7 @@ import rehypeKatexSvelte from 'rehype-katex-svelte';
|
|||
// import github from "remark-github";
|
||||
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
|
||||
// import rehypeToc from '@jsdevtools/rehype-toc';
|
||||
import { createHighlighter } from "@bitmachina/highlighter";
|
||||
|
||||
import { parse, format } from "node:path";
|
||||
|
|
@ -92,6 +92,110 @@ const httpHighlight = {
|
|||
}
|
||||
|
||||
const hrefTemplate = (/** @type {string} */ permalink) => `#${permalink}`
|
||||
|
||||
// function customizeTOC(toc) {
|
||||
// // console.log(toc)
|
||||
|
||||
// return {
|
||||
// type: 'root',
|
||||
// children: [{
|
||||
// type: "element",
|
||||
// // tagName: "svelte:component",
|
||||
// // properties: { this: "{tocComponent}" },
|
||||
// tagName: "div",
|
||||
// properties: {},
|
||||
// children: [toc],
|
||||
// }]
|
||||
// };
|
||||
// }
|
||||
function buildNestedHeadings(headings) {
|
||||
let result = [];
|
||||
let stack = [{ level: 0, children: result }];
|
||||
|
||||
for (let heading of headings) {
|
||||
while (
|
||||
stack.length > 1 &&
|
||||
heading.level <= stack[stack.length - 1].level
|
||||
) {
|
||||
stack.pop();
|
||||
}
|
||||
let parent = stack[stack.length - 1];
|
||||
let newHeading = {
|
||||
...heading,
|
||||
children: [],
|
||||
level: heading.level,
|
||||
};
|
||||
parent.children.push(newHeading);
|
||||
stack.push(newHeading);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { toString as mdast_tree_to_string } from 'mdast-util-to-string'
|
||||
|
||||
|
||||
import GithubSlugger from 'github-slugger'
|
||||
/**
|
||||
* @param {{ prefix?: string; }} opts
|
||||
*/
|
||||
function add_toc_remark(opts) {
|
||||
const slugs = new GithubSlugger()
|
||||
const prefix = opts?.prefix || "";
|
||||
return async function transformer(tree, vFile) {
|
||||
slugs.reset()
|
||||
|
||||
vFile.data.flattenedHeadings = [];
|
||||
|
||||
visit(tree, 'heading', (node) => {
|
||||
let title = mdast_tree_to_string(node);
|
||||
vFile.data.flattenedHeadings.push({
|
||||
level: node.depth,
|
||||
title,
|
||||
id: prefix + slugs.slug(title)
|
||||
});
|
||||
});
|
||||
|
||||
if (!vFile.data.fm) vFile.data.fm = {};
|
||||
vFile.data.fm.flattenedHeadings = vFile.data.flattenedHeadings;
|
||||
vFile.data.fm.headings = buildNestedHeadings(vFile.data.flattenedHeadings);
|
||||
};
|
||||
}
|
||||
import { toString as hast_tree_to_string } from 'hast-util-to-string'
|
||||
/**
|
||||
* Determines whether the given node is an HTML element.
|
||||
*/
|
||||
function isHtmlElementNode(node) {
|
||||
return typeof node === "object" &&
|
||||
node.type === "element" &&
|
||||
typeof node.tagName === "string" &&
|
||||
"properties" in node &&
|
||||
typeof node.properties === "object";
|
||||
}
|
||||
const HEADINGS = ["h1", "h2", "h3", "h4", "h5", "h6"]
|
||||
/**
|
||||
* Determines whether the given node is an HTML heading node, according to the specified options
|
||||
*/
|
||||
function isHeadingNode(node) {
|
||||
return isHtmlElementNode(node) && HEADINGS.includes(node.tagName);
|
||||
}
|
||||
function add_toc_rehype(self, opts) {
|
||||
return async function transformer(tree, vFile) {
|
||||
// console.log(tree)
|
||||
vFile.data.headings = [];
|
||||
|
||||
visit(tree, isHeadingNode, (node) => {
|
||||
console.log(node)
|
||||
vFile.data.headings.push({
|
||||
level: node.depth,
|
||||
title: hast_tree_to_string(node),
|
||||
});
|
||||
});
|
||||
|
||||
if (!vFile.data.fm) vFile.data.fm = {};
|
||||
vFile.data.fm.headings = vFile.data.headings;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @type {import("mdsvex").MdsvexOptions}
|
||||
*/
|
||||
|
|
@ -104,6 +208,10 @@ const config = {
|
|||
dashes: "oldschool",
|
||||
},
|
||||
|
||||
// layout: {
|
||||
// _: "./src/layout.svelte"
|
||||
// },
|
||||
|
||||
highlight: {
|
||||
// @ts-ignore
|
||||
highlighter: await createHighlighter({ theme: "github-dark", langs: [httpHighlight] }),
|
||||
|
|
@ -149,12 +257,13 @@ const config = {
|
|||
// }],
|
||||
// [remarkBibliography, { bibliography }],
|
||||
// [remarkMermaid, {}]
|
||||
[add_toc_remark, { prefix: "h-" }]
|
||||
],
|
||||
rehypePlugins: [
|
||||
// @ts-ignore
|
||||
rehypeKatexSvelte,
|
||||
// @ts-ignore
|
||||
rehypeSlug
|
||||
[rehypeSlug, { prefix: "h-" }],
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@
|
|||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/sharedworker": "^0.0.115",
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^10.4.1",
|
||||
"hast-util-to-string": "^3.0.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mdsvex": "^0.11.2",
|
||||
"rehype-katex-svelte": "1.2",
|
||||
"rehype-slug": "^6.0.0",
|
||||
|
|
@ -44,6 +47,7 @@
|
|||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.4.5",
|
||||
"unified": "^11.0.4",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-dynamic-import": "^1.5.0",
|
||||
"vite-plugin-image-optimizer": "^1.1.8"
|
||||
|
|
|
|||
78
packages/website/src/lib/Toc.svelte
Normal file
78
packages/website/src/lib/Toc.svelte
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<script lang="ts">
|
||||
import TocItem from "./TocItem.svelte";
|
||||
let className = "toc";
|
||||
type FlatHeading = { level: number; title: string };
|
||||
export let headings: nestedListNode[];
|
||||
|
||||
// creates a `class` property, even
|
||||
// though it is a reserved word
|
||||
export { className as class };
|
||||
export let listType = "ul";
|
||||
|
||||
let open = false;
|
||||
/** @type {import('./$types').Snapshot<string>} */
|
||||
export const snapshot = {
|
||||
capture: () => open,
|
||||
restore: (value: boolean) => (open = value),
|
||||
};
|
||||
|
||||
// console.log(headings);
|
||||
</script>
|
||||
|
||||
<aside class={className}>
|
||||
<details bind:open>
|
||||
<summary accesskey="c" title="(Alt + C)">Table of Contents</summary>
|
||||
<div class="inner">
|
||||
{#if headings?.length > 0}
|
||||
<svelte:element
|
||||
this={listType}
|
||||
class="toc-level {'toc-level-' + headings[0].level}"
|
||||
>
|
||||
{#each headings as node}
|
||||
<TocItem {node} {listType} />
|
||||
{/each}
|
||||
</svelte:element>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
aside {
|
||||
margin-block: calc(var(--spacing) / 4);
|
||||
}
|
||||
details {
|
||||
/* margin: var(--spacing) 2px; */
|
||||
margin: 0 2px;
|
||||
border: 1px solid var(--surface-secondary-color);
|
||||
background: var(--surface-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.4em;
|
||||
}
|
||||
details summary {
|
||||
cursor: zoom-in;
|
||||
margin-inline-start: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
details[open] summary {
|
||||
cursor: zoom-out;
|
||||
}
|
||||
summary {
|
||||
font-weight: 500;
|
||||
}
|
||||
.inner {
|
||||
padding: 0 10px;
|
||||
opacity: 0.9;
|
||||
margin-block-start: calc(var(--spacing) / 4);
|
||||
margin-block-end: calc(var(--spacing) / 2);
|
||||
margin-inline: calc(var(--spacing) / 2);
|
||||
}
|
||||
.inner :global(ul) {
|
||||
margin: 0;
|
||||
margin-inline-start: calc(var(--spacing));
|
||||
padding: 0;
|
||||
}
|
||||
summary:focus {
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
||||
16
packages/website/src/lib/TocItem.svelte
Normal file
16
packages/website/src/lib/TocItem.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
|
||||
export let node: nestedListNode;
|
||||
export let listType = "ul"
|
||||
</script>
|
||||
|
||||
<li class="toc-item {"toc-item-" + node.level}">
|
||||
<a href={"#" + node.id} class="toc-link {"toc-link-" + node.level}">{node.title}</a>
|
||||
{#if node.children.length > 0}
|
||||
<svelte:element this={listType} class="toc-level {"toc-level-" + node.children[0].level}">
|
||||
{#each node.children as nodes}
|
||||
<svelte:self node={nodes} {listType} />
|
||||
{/each}
|
||||
</svelte:element>
|
||||
{/if}
|
||||
</li>
|
||||
7
packages/website/src/lib/toc.d.ts
vendored
Normal file
7
packages/website/src/lib/toc.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
type nestedListNode = {
|
||||
title: string;
|
||||
id: string;
|
||||
level: number;
|
||||
children: nestedListNode[];
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
import SvelteSeo from "svelte-seo";
|
||||
export let data;
|
||||
import { SITE_URL } from "$lib/metadata";
|
||||
import Toc from "$lib/Toc.svelte";
|
||||
// let GhReleasesDownload: Promise<any>;
|
||||
// if (data.ghReleaseData) {
|
||||
// GhReleasesDownload = import("$lib/GhReleasesDownload.svelte").then((m) => m.default)
|
||||
|
|
@ -29,17 +30,25 @@
|
|||
|
||||
<article class="h-entry">
|
||||
<h1 id="title" class="p-name">{data.post.title}</h1>
|
||||
<aside>Published on <time class="dt-published" datetime={data.post.date}>{new Date(data.post.date).toLocaleDateString()}</time></aside>
|
||||
<aside>
|
||||
Published on <time class="dt-published" datetime={data.post.date}
|
||||
>{new Date(data.post.date).toLocaleDateString()}</time
|
||||
>
|
||||
</aside>
|
||||
<Toc headings={data.post.headings} />
|
||||
<!-- {#await GhReleasesDownload}
|
||||
|
||||
{:then component}
|
||||
<svelte:component this={component} releaseData={data.ghReleaseData} />
|
||||
{/await} -->
|
||||
|
||||
<!-- <div class="e-content"> -->
|
||||
<svelte:component this={data.component} />
|
||||
<div class="e-content">
|
||||
<svelte:component this={data.component} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
aside {font-size: .85em;}
|
||||
</style>
|
||||
aside {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
|
|
@ -113,9 +113,18 @@ importers:
|
|||
'@types/sharedworker':
|
||||
specifier: ^0.0.115
|
||||
version: 0.0.115
|
||||
github-slugger:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
glob:
|
||||
specifier: ^10.4.1
|
||||
version: 10.4.1
|
||||
hast-util-to-string:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
mdast-util-to-string:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
mdsvex:
|
||||
specifier: ^0.11.2
|
||||
version: 0.11.2(svelte@4.2.18)
|
||||
|
|
@ -182,6 +191,9 @@ importers:
|
|||
unified:
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
vite:
|
||||
specifier: ^5.3.1
|
||||
version: 5.3.1(@types/node@20.14.2)(terser@5.31.1)
|
||||
|
|
@ -934,7 +946,7 @@ packages:
|
|||
'@codemirror/view': '>=6.0.0'
|
||||
|
||||
Notes@file:packages/website/Notes-1.0.0.tgz:
|
||||
resolution: {integrity: sha512-vppkFly1DinPtUJSGlwbSWHXq7R9ozQoZHrWLjsnIDOvsbXYezdHm+KquuWVIEkEveDmg6SODp49j1Bsflnyzw==, tarball: file:packages/website/Notes-1.0.0.tgz}
|
||||
resolution: {integrity: sha512-5ly2YBmx1v2j7S5QtOTzZbTY5WJv1/1JNKGXOle8VtgRwg1WvOohRqDSaySiypHDWOGebqLq9KbxIr/Y6VCUnw==, tarball: file:packages/website/Notes-1.0.0.tgz}
|
||||
version: 1.0.0
|
||||
|
||||
acorn@8.12.0:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue