diff --git a/docs/src/content/pages/posts/markdown-style-guide.md b/docs/src/content/pages/posts/markdown-style-guide.md index 0bbc5cd..68a29f9 100644 --- a/docs/src/content/pages/posts/markdown-style-guide.md +++ b/docs/src/content/pages/posts/markdown-style-guide.md @@ -12,17 +12,17 @@ Here is a sample of some basic Markdown syntax that can be used when writing Mar The following HTML `

`—`

` elements represent six levels of section headings. `

` is the highest section level while `

` is the lowest. -# H1 +

H1

-## H2 +

H2

-### H3 +

H3

-#### H4 +

H4

-##### H5 +
H5
-###### H6 +
H6
## Paragraph diff --git a/package.json b/package.json index 6bfe711..3c8214e 100644 --- a/package.json +++ b/package.json @@ -55,14 +55,14 @@ "@astrojs/markdown-remark": "^5.2.0", "@astrojs/mdx": "^3.1.7", "@astrojs/sitemap": "^3.2.0", - "@microflash/rehype-toc": "^1.0.2", "bcp-47": "^2.1.0", "dayjs": "^1.11.13", "fast-glob": "^3.3.2", - "hastscript": "^9.0.0", + "hast-util-find-and-replace": "^5.0.1", "i18next": "^23.15.2", "nprogress": "^0.2.0", - "rehype-autolink-headings": "^7.1.0" + "rehype-autolink-headings": "^7.1.0", + "unist-util-visit": "^5.0.0" }, "devDependencies": { "@antfu/eslint-config": "^3.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a68a33..cb547a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@iconify/json': specifier: ^2.2.256 version: 2.2.256 - '@microflash/rehype-toc': - specifier: ^1.0.2 - version: 1.0.2 '@unocss/reset': specifier: ^0.63.2 version: 0.63.3 @@ -35,9 +32,9 @@ importers: fast-glob: specifier: ^3.3.2 version: 3.3.2 - hastscript: - specifier: ^9.0.0 - version: 9.0.0 + hast-util-find-and-replace: + specifier: ^5.0.1 + version: 5.0.1 i18next: specifier: ^23.15.2 version: 23.15.2 @@ -47,6 +44,9 @@ importers: rehype-autolink-headings: specifier: ^7.1.0 version: 7.1.0 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 devDependencies: '@antfu/eslint-config': specifier: ^3.7.3 @@ -944,9 +944,6 @@ packages: '@mdx-js/mdx@3.0.1': resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==} - '@microflash/rehype-toc@1.0.2': - resolution: {integrity: sha512-Vx3MAlgjUpj758rYYBRHxePHNadJzCgVT2/FMpPd2vRkgucpXSQ8DhCYEhp8Zrlc1jWjU8bSUsROQvZ1tyA76A==} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1130,9 +1127,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/hast@2.3.10': - resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -2316,8 +2310,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-find-and-replace@4.1.3: - resolution: {integrity: sha512-q8UYl9o5wvg+ZT666rb/xYmCBgTS1wQnoadz1Ugc82XLMx2AGPqIgDVmdf4wxYN7evZFXT5FwmvWeJhSbZ4nIA==} + hast-util-find-and-replace@5.0.1: + resolution: {integrity: sha512-S12fTskO3Hf2IGCBWXs1UcXT8GEJ3jmvmPZJctkRwfl3a8jnGi8aFYT8kd2zcEH+VE0qcGgKF0ewt5BPAsfIhw==} hast-util-from-html@2.0.3: resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} @@ -2325,24 +2319,12 @@ packages: hast-util-from-parse5@8.0.1: resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} - hast-util-has-property@2.0.1: - resolution: {integrity: sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==} - - hast-util-heading-rank@2.1.1: - resolution: {integrity: sha512-iAuRp+ESgJoRFJbSyaqsfvJDY6zzmFoEnL1gtz1+U8gKtGGj1p0CVlysuUAUjq95qlZESHINLThwJzNGmgGZxA==} - hast-util-heading-rank@3.0.0: resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} - hast-util-is-element@2.1.3: - resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} - hast-util-is-element@3.0.0: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - hast-util-parse-selector@3.1.1: - resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -2361,24 +2343,15 @@ packages: hast-util-to-parse5@8.0.0: resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} - hast-util-to-string@2.0.0: - resolution: {integrity: sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==} - hast-util-to-text@4.0.2: resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hastscript@7.2.0: - resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} - hastscript@8.0.0: resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} - hastscript@9.0.0: - resolution: {integrity: sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==} - hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -3678,9 +3651,6 @@ packages: unist-util-find-after@5.0.0: resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - unist-util-is@5.2.1: - resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} - unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -3702,15 +3672,9 @@ packages: unist-util-visit-children@3.0.0: resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} - unist-util-visit-parents@5.1.3: - resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} - unist-util-visit-parents@6.0.1: resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} - unist-util-visit@4.1.2: - resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} - unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} @@ -4880,15 +4844,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@microflash/rehype-toc@1.0.2': - dependencies: - hast-util-find-and-replace: 4.1.3 - hast-util-has-property: 2.0.1 - hast-util-heading-rank: 2.1.1 - hast-util-to-string: 2.0.0 - hastscript: 7.2.0 - unist-util-visit: 4.1.2 - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5074,10 +5029,6 @@ snapshots: '@types/estree@1.0.6': {} - '@types/hast@2.3.10': - dependencies: - '@types/unist': 2.0.11 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -6700,11 +6651,12 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-find-and-replace@4.1.3: + hast-util-find-and-replace@5.0.1: dependencies: + '@types/hast': 3.0.4 escape-string-regexp: 5.0.0 - hast-util-is-element: 2.1.3 - unist-util-visit-parents: 5.1.3 + hast-util-is-element: 3.0.0 + unist-util-visit-parents: 6.0.1 hast-util-from-html@2.0.3: dependencies: @@ -6726,29 +6678,14 @@ snapshots: vfile-location: 5.0.3 web-namespaces: 2.0.1 - hast-util-has-property@2.0.1: {} - - hast-util-heading-rank@2.1.1: - dependencies: - '@types/hast': 2.3.10 - hast-util-heading-rank@3.0.0: dependencies: '@types/hast': 3.0.4 - hast-util-is-element@2.1.3: - dependencies: - '@types/hast': 2.3.10 - '@types/unist': 2.0.11 - hast-util-is-element@3.0.0: dependencies: '@types/hast': 3.0.4 - hast-util-parse-selector@3.1.1: - dependencies: - '@types/hast': 2.3.10 - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -6834,10 +6771,6 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 - hast-util-to-string@2.0.0: - dependencies: - '@types/hast': 2.3.10 - hast-util-to-text@4.0.2: dependencies: '@types/hast': 3.0.4 @@ -6849,14 +6782,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hastscript@7.2.0: - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 3.1.1 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - hastscript@8.0.0: dependencies: '@types/hast': 3.0.4 @@ -6865,14 +6790,6 @@ snapshots: property-information: 6.5.0 space-separated-tokens: 2.0.2 - hastscript@9.0.0: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 4.0.0 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - hosted-git-info@2.8.9: {} html-escaper@2.0.2: {} @@ -8482,10 +8399,6 @@ snapshots: '@types/unist': 3.0.3 unist-util-is: 6.0.0 - unist-util-is@5.2.1: - dependencies: - '@types/unist': 2.0.11 - unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -8516,22 +8429,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@5.1.3: - dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - unist-util-visit-parents@6.0.1: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.0 - unist-util-visit@4.1.2: - dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 diff --git a/src/components/Toc.astro b/src/components/Toc.astro new file mode 100644 index 0000000..3c5d29b --- /dev/null +++ b/src/components/Toc.astro @@ -0,0 +1,19 @@ +--- +import { TocHeading } from '../utils/toc' +import TocItem from './TocItem.astro' + +export interface Props { + headings: TocHeading[] +} + +const { headings } = Astro.props +--- + +
+
+
+
+ +
diff --git a/src/components/TocItem.astro b/src/components/TocItem.astro new file mode 100644 index 0000000..5a9b71c --- /dev/null +++ b/src/components/TocItem.astro @@ -0,0 +1,22 @@ +--- +import { TocHeading } from '../utils/toc' + +export type Props = TocHeading + +const { slug, text, children } = Astro.props +--- + +
  • + + {text} + + { + !!children?.length && ( + + ) + } +
  • diff --git a/src/components/WrapperPost.astro b/src/components/WrapperPost.astro index 0c3f6de..73839ff 100644 --- a/src/components/WrapperPost.astro +++ b/src/components/WrapperPost.astro @@ -3,12 +3,21 @@ import type { Props } from '../props' import config from 'virtual:vitesse/user-config' import { formatDate } from '../logics' +import { generateToc } from '../utils/toc' import DraftContentNotice from './DraftContentNotice.astro' import FallbackContentNotice from './FallbackContentNotice.astro' +import Toc from './Toc.astro' const { entry } = Astro.props const { data: frontmatter } = entry +const { + remarkPluginFrontmatter: { hasToc }, + headings, +} = await entry.render() + +const toc = generateToc(headings ?? [], 1, 4) + let tweetUrl = '' let elkUrl = '' @@ -59,6 +68,13 @@ if (config.social?.mastodon) { > {Astro.props.entry.data.draft && } {Astro.props.isFallback && } + { + hasToc && ( +
    + +
    + ) + } { diff --git a/src/index.ts b/src/index.ts index fa3fe5d..297cf57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ * loaded when a user imports the Vitesse integration in their Astro configuration file. These * directives must be first at the top of the file and can only be preceded by this comment. */ -/// /// /// /// @@ -13,12 +12,11 @@ import type { PluginTranslations, VitesseUserConfigWithPlugins } from './utils/p import { rehypeHeadingIds } from '@astrojs/markdown-remark' import mdx from '@astrojs/mdx' -import rehypeToc from '@microflash/rehype-toc' -import { h } from 'hastscript' import rehypeAutolinkHeadings from 'rehype-autolink-headings' import { vitesseSitemap } from './integrations/sitemap' import { vitePluginVitesseUserConfig } from './integrations/virtual-user-config' +import { rehypeToc } from './plugins/rehype-toc' import { processI18nConfig } from './utils/i18n' import { injectPluginTranslationsTypes, runPlugins } from './utils/plugins' import { createTranslationSystemFromFs } from './utils/translations-fs' @@ -101,26 +99,8 @@ export default function VitesseIntegration({ }, markdown: { rehypePlugins: [ + rehypeToc, rehypeHeadingIds, - [ - rehypeToc, - { - toc: (headings: { id: string, title: string, depth: number }[]) => { - return h('section', [ - h('div.table-of-contents', [ - h('div.table-of-contents-anchor', [ - h('div.i-ri-menu-2-fill'), - ]), - h('ul', [ - ...headings.map(heading => h(`li`, [ - h('a', { href: `#${heading.id}` }, heading.title), - ])), - ]), - ]), - ]) - }, - }, - ], [ rehypeAutolinkHeadings, { diff --git a/src/plugins/rehype-toc.ts b/src/plugins/rehype-toc.ts new file mode 100644 index 0000000..d2b69c0 --- /dev/null +++ b/src/plugins/rehype-toc.ts @@ -0,0 +1,19 @@ +import type { RehypePlugin } from '@astrojs/markdown-remark' +import { findAndReplace } from 'hast-util-find-and-replace' +import { visit } from 'unist-util-visit' + +export const rehypeToc: RehypePlugin = () => { + return (tree, file: any) => { + let replaced = false + visit(tree, (node) => { + findAndReplace(node, [/\[\[toc\]\]/gi, (text) => { + if (!replaced) { + file.data.astro.frontmatter.hasToc = true + replaced = true + return + } + return text // Return the original text if already replaced + }]) + }) + } +} diff --git a/src/rehype-toc.d.ts b/src/rehype-toc.d.ts deleted file mode 100644 index 800c85c..0000000 --- a/src/rehype-toc.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module '@microflash/rehype-toc' { - import type { Plugin } from 'unified' - import type { Node } from 'unist' - - interface RehypeTocOptions { - matcher?: RegExp - id?: string - toc?: (headings: { id: string, title: string, depth: number }[]) => Node - } - - const rehypeToc: Plugin<[RehypeTocOptions?]> - - export default rehypeToc -} diff --git a/src/utils/toc.ts b/src/utils/toc.ts new file mode 100644 index 0000000..ce3ea5f --- /dev/null +++ b/src/utils/toc.ts @@ -0,0 +1,38 @@ +import type { MarkdownHeading } from 'astro' + +export interface TocHeading extends MarkdownHeading { + children?: TocHeading[] +} + +export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6 + +export function generateToc( + headings: readonly TocHeading[], + minHeadingLevel: number, + maxHeadingLevel: number, +): TocHeading[] { + const filteredItems = headings.filter(item => item.depth >= minHeadingLevel && item.depth <= maxHeadingLevel) + const stack: TocHeading[] = [] + const result: TocHeading[] = [] + + for (const item of filteredItems) { + item.children = [] + + while (stack.length > 0 && stack[stack.length - 1].depth >= item.depth) { + stack.pop() + } + + if (stack.length === 0) { + result.push(item) + } + else { + const parent = stack[stack.length - 1] + parent.children = parent.children || [] + parent.children.push(item) + } + + stack.push(item) + } + + return result +}