diff --git a/packages/website/mdsvex.config.js b/packages/website/mdsvex.config.js index 93e22b6d..0e0c469e 100644 --- a/packages/website/mdsvex.config.js +++ b/packages/website/mdsvex.config.js @@ -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-" }], ], }; diff --git a/packages/website/package.json b/packages/website/package.json index e25727d2..a074bd28 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -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" diff --git a/packages/website/src/lib/Toc.svelte b/packages/website/src/lib/Toc.svelte new file mode 100644 index 00000000..d0aa65c7 --- /dev/null +++ b/packages/website/src/lib/Toc.svelte @@ -0,0 +1,78 @@ + + + + + diff --git a/packages/website/src/lib/TocItem.svelte b/packages/website/src/lib/TocItem.svelte new file mode 100644 index 00000000..626f825a --- /dev/null +++ b/packages/website/src/lib/TocItem.svelte @@ -0,0 +1,16 @@ + + +
  • +{node.title} +{#if node.children.length > 0} + + {#each node.children as nodes} + + {/each} + +{/if} +
  • \ No newline at end of file diff --git a/packages/website/src/lib/toc.d.ts b/packages/website/src/lib/toc.d.ts new file mode 100644 index 00000000..c7eab9c3 --- /dev/null +++ b/packages/website/src/lib/toc.d.ts @@ -0,0 +1,7 @@ + +type nestedListNode = { + title: string; + id: string; + level: number; + children: nestedListNode[]; +}; \ No newline at end of file diff --git a/packages/website/src/routes/blog/[...date]/[slug]/+page.svelte b/packages/website/src/routes/blog/[...date]/[slug]/+page.svelte index 761557a2..07ee6210 100644 --- a/packages/website/src/routes/blog/[...date]/[slug]/+page.svelte +++ b/packages/website/src/routes/blog/[...date]/[slug]/+page.svelte @@ -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; // if (data.ghReleaseData) { // GhReleasesDownload = import("$lib/GhReleasesDownload.svelte").then((m) => m.default) @@ -29,17 +30,25 @@

    {data.post.title}

    - + + - - +
    + +
    \ No newline at end of file + aside { + font-size: 0.85em; + } + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfb3df54..c84a847b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: