diff --git a/package-lock.json b/package-lock.json index 7b5ca745..afc6c5fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,11 @@ "": { "name": "harambasic.de", "version": "3.0.0", + "dependencies": { + "github-slugger": "^2.0.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-slug": "^6.0.0" + }, "devDependencies": { "@fontsource/fira-mono": "^5.0.8", "@iconify/svelte": "^3.1.4", @@ -1438,7 +1443,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.3.tgz", "integrity": "sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==", - "dev": true, "dependencies": { "@types/unist": "*" } @@ -1532,8 +1536,7 @@ "node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", - "dev": true + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.16.0", @@ -1728,8 +1731,7 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitest/expect": { "version": "0.34.6", @@ -2022,7 +2024,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -2727,7 +2728,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -2760,7 +2760,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dev": true, "dependencies": { "dequal": "^2.0.0" }, @@ -3165,8 +3164,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extend-shallow": { "version": "2.0.1", @@ -3432,6 +3430,11 @@ "node": "*" } }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3611,11 +3614,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-is-element": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", - "dev": true, "dependencies": { "@types/hast": "^3.0.0" }, @@ -3705,6 +3719,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", + "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-text": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.0.tgz", @@ -3936,7 +3962,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, "engines": { "node": ">=12" }, @@ -5842,6 +5867,23 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-highlight": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.0.tgz", @@ -5859,6 +5901,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-stringify": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.0.tgz", @@ -6632,7 +6690,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", - "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -6730,7 +6787,6 @@ "version": "11.0.4", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", - "dev": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -6818,7 +6874,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dev": true, "dependencies": { "@types/unist": "^3.0.0" }, @@ -6844,7 +6899,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dev": true, "dependencies": { "@types/unist": "^3.0.0" }, @@ -6857,7 +6911,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "dev": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", @@ -6872,7 +6925,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "dev": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" @@ -6931,7 +6983,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dev": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0", @@ -6960,7 +7011,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "dev": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" diff --git a/package.json b/package.json index 97133922..62a8572b 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "prettier-plugin-svelte": "^3.0.3", "rehype-highlight": "^7.0.0", "rehype-stringify": "^10.0.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-slug": "^6.0.0", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "remark-parse-frontmatter": "^1.0.3", @@ -61,7 +63,10 @@ "typescript": "^5.2.2", "unist-util-visit": "^5.0.0", "vite": "^4.4.9", - "vitest": "^0.34.4" + "vitest": "^0.34.4", + "github-slugger": "^2.0.0" }, - "type": "module" + "type": "module", + "dependencies": { + } } diff --git a/src/lib/data/posts/helper.ts b/src/lib/data/posts/helper.ts index 6e025a6a..35d10443 100644 --- a/src/lib/data/posts/helper.ts +++ b/src/lib/data/posts/helper.ts @@ -33,7 +33,6 @@ export function getPost(entry: any): Post { const type = EntryType.Post const slug = getSlug(meta.title) const relativePath = `/${type.toLowerCase()}s/${slug}` - // TODO add toc: getNestedToc(entry.getHeadings()), return { type, title: meta.title, @@ -44,49 +43,10 @@ export function getPost(entry: any): Post { updated: getDate(meta.updated), tldr: meta.tldr, discussion: meta.discussion, - toc: [], + toc: entry.toc, slug, relativePath, fullPath: `https://harambasic.de${relativePath}`, html: entry.html } -} - -// TODO test -// TODO can this be rewritten in a nicer way? -// provided by https://codepen.io/Frnak/pen/mdmEjyG?editors=0011 -// TODO fix markdownHeadings any -// check if this might be a nicer solution: https://github.com/ryanfiller/portfolio-svelte/blob/main/src/plugins/rehype/table-of-contents.js#L29 -export function getNestedToc(markdownHeading: any): TocNode[] { - let latestEntry: TocNode | null - let latestParent: TocNode | null - const markdownHeadingCopy = JSON.parse(JSON.stringify(markdownHeading)) - if (markdownHeadingCopy.length <= 1) return markdownHeadingCopy - // TODO fix any - const entryDepth: number[] = markdownHeading.reduce((acc: number, item: any) => { - return item.depth < acc ? item.depth : acc - }, Number.POSITIVE_INFINITY) - // TODO fix any - return markdownHeadingCopy.reduce((result: any, entry: any) => { - if (latestEntry && !latestEntry.children) { - latestEntry.children = [] - } - const latestEntryDepth = latestEntry?.depth || 0 - const latestEntryChildren = latestEntry?.children || [] - const latestParentChildren = latestParent?.children || [] - if (entry.depth === entryDepth) { - entry.children = [] - result.push(entry) - latestParent = null - } else if (entry.depth === latestEntryDepth + 1) { - latestEntryChildren.push(entry) - latestParent = latestEntry - } else if (entry.depth === latestEntryDepth) { - latestParentChildren.push(entry) - } else { - console.error('Unexpected Toc behaviour', entry) - } - latestEntry = entry - return result - }, []) -} +} \ No newline at end of file diff --git a/src/lib/types/post.d.ts b/src/lib/types/post.d.ts index a951c512..5e6af4bc 100644 --- a/src/lib/types/post.d.ts +++ b/src/lib/types/post.d.ts @@ -3,7 +3,7 @@ import type { Entry } from './entry' export interface TocNode { depth: number slug: string - text: string + value: string children: TocNode[] | null } diff --git a/src/lib/util/converter.server.ts b/src/lib/util/converter.server.ts index 69e3c056..6c473ba3 100644 --- a/src/lib/util/converter.server.ts +++ b/src/lib/util/converter.server.ts @@ -1,4 +1,5 @@ import type { EntryType } from '$lib/types/enums' +import type { Node } from 'unist'; import { join } from 'path' import * as fs from 'fs/promises' import { remark } from 'remark' @@ -7,9 +8,25 @@ import remarkParseFrontmatter from 'remark-parse-frontmatter' import remarkRehype from 'remark-rehype' import rehypeStringify from 'rehype-stringify' import rehypeHighlight from 'rehype-highlight' +import rehypeAutolinkHeadings from 'rehype-autolink-headings' +import rehypeSlug from 'rehype-slug' import { visit } from 'unist-util-visit' +import type { VFile } from 'remark-rehype/lib'; +import type { TocNode } from '$lib/types/post' +import { slug as slugger } from 'github-slugger'; -const processor = remark().use(remarkFrontmatter).use(remarkParseFrontmatter).use(remarkRehype).use(_enhanceImage).use(rehypeHighlight).use(rehypeStringify).freeze() +// todo maybe markdown file? +const processor = remark() + .use(remarkFrontmatter) + .use(remarkParseFrontmatter) + .use(_remarkGenerateNestedToc) + .use(remarkRehype) + .use(rehypeSlug) + .use(rehypeAutolinkHeadings) + .use(_rehypeEnhanceImage) + .use(rehypeHighlight) + .use(rehypeStringify) + .freeze() // TODO still needs to be transformed to the corresponding Entry types export async function getRawEntries(entryType: EntryType): Promise { @@ -17,7 +34,7 @@ export async function getRawEntries(entryType: EntryType): Promise { const entries = await Promise.all( files.map(async (file) => { const output = processor.processSync(file) - return { html: output.value, meta: output.data.frontmatter } + return { html: output.value, meta: output.data.frontmatter, toc: output.data.toc } }) ) return entries @@ -35,7 +52,8 @@ export async function _getFiles(entryType: EntryType): Promise { ) } -function _enhanceImage() { +// todo maybe markdown file? +function _rehypeEnhanceImage() { console.log('_enhanceImage') return (tree: any) => { visit(tree, 'element', (node) => { @@ -48,4 +66,58 @@ function _enhanceImage() { } }}); }; +} + +// todo maybe markdown file? +interface HeadingNode extends Node { + depth: number; + children: { value: string }[]; +} + +// todo maybe markdown file? +function _remarkGenerateNestedToc() { + console.log('------------------') + return (tree: Node, file: VFile) => { + const headings: { value: string, depth: number, slug: string }[] = [] + visit(tree, 'heading', (node: HeadingNode) => { + const value = node.children.reduce((text, child) => text + child.value, '') + const slug = slugger(value) + headings.push({ value, depth: node.depth, slug }) + }); + file.data.toc = _getNestedToc(headings) + }; +}; + +function _getNestedToc(markdownHeading: any): TocNode[] { + let latestEntry: TocNode | null + let latestParent: TocNode | null + const markdownHeadingCopy = JSON.parse(JSON.stringify(markdownHeading)) + if (markdownHeadingCopy.length <= 1) return markdownHeadingCopy + // TODO fix any + const entryDepth: number[] = markdownHeading.reduce((acc: number, item: any) => { + return item.depth < acc ? item.depth : acc + }, Number.POSITIVE_INFINITY) + // TODO fix any + return markdownHeadingCopy.reduce((result: any, entry: any) => { + if (latestEntry && !latestEntry.children) { + latestEntry.children = [] + } + const latestEntryDepth = latestEntry?.depth || 0 + const latestEntryChildren = latestEntry?.children || [] + const latestParentChildren = latestParent?.children || [] + if (entry.depth === entryDepth) { + entry.children = [] + result.push(entry) + latestParent = null + } else if (entry.depth === latestEntryDepth + 1) { + latestEntryChildren.push(entry) + latestParent = latestEntry + } else if (entry.depth === latestEntryDepth) { + latestParentChildren.push(entry) + } else { + console.error('Unexpected Toc behaviour', entry) + } + latestEntry = entry + return result + }, []) } \ No newline at end of file diff --git a/src/routes/(main)/posts/[slug]/Entry.svelte b/src/routes/(main)/posts/[slug]/Entry.svelte index e8266cf1..4b8c7371 100644 --- a/src/routes/(main)/posts/[slug]/Entry.svelte +++ b/src/routes/(main)/posts/[slug]/Entry.svelte @@ -1,12 +1,13 @@
@@ -22,12 +23,11 @@ {/each} - - +
{@html tldr} @@ -91,13 +91,13 @@ gap: var(--xs); } } - /* .toc { + .toc { grid-area: toc; .content { position: sticky; top: var(--l); } - } */ + } .tldr { grid-area: tldr; } diff --git a/src/routes/(main)/posts/[slug]/TableOfContent.svelte b/src/routes/(main)/posts/[slug]/TableOfContent.svelte index d5e50f6a..373b5c3a 100644 --- a/src/routes/(main)/posts/[slug]/TableOfContent.svelte +++ b/src/routes/(main)/posts/[slug]/TableOfContent.svelte @@ -15,15 +15,15 @@
- diff --git a/src/routes/(main)/posts/[slug]/TableOfContentNode.svelte b/src/routes/(main)/posts/[slug]/TableOfContentNode.svelte index 0a8c6efc..b40c897d 100644 --- a/src/routes/(main)/posts/[slug]/TableOfContentNode.svelte +++ b/src/routes/(main)/posts/[slug]/TableOfContentNode.svelte @@ -6,7 +6,7 @@
  • - {node.text} + {node.value} {#if node.children && node.children.length !== 0}
      @@ -18,15 +18,19 @@