diff --git a/.gitignore b/.gitignore index baeb722..ca9e8e9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist .eslintcache *.log* *.env* +.pnpm-store diff --git a/package.json b/package.json index a90df76..3f0b666 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "defu": "^6.1.4", "destr": "^2.0.3", "didyoumean2": "^6.0.1", + "github-slugger": "^2.0.0", "globby": "^14.0.2", "magic-string": "^0.30.11", "mdbox": "^0.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d408b2..44d5ec1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: didyoumean2: specifier: ^6.0.1 version: 6.0.1 + github-slugger: + specifier: ^2.0.0 + version: 2.0.0 globby: specifier: ^14.0.2 version: 14.0.2 @@ -1640,6 +1643,9 @@ packages: resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} hasBin: true + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -4226,6 +4232,8 @@ snapshots: pathe: 1.1.2 tar: 6.2.1 + github-slugger@2.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 diff --git a/src/generators/index.ts b/src/generators/index.ts index 1c7f6a7..f9513e0 100644 --- a/src/generators/index.ts +++ b/src/generators/index.ts @@ -7,6 +7,7 @@ import { jsimport } from "./jsimport"; import { withAutomd } from "./with-automd"; import { file } from "./file"; import { contributors } from "./contributors"; +import { toc } from "./toc"; export default { jsdocs, @@ -19,4 +20,5 @@ export default { jsimport, "with-automd": withAutomd, contributors, + toc, } as Record; diff --git a/src/generators/toc.ts b/src/generators/toc.ts new file mode 100644 index 0000000..c5e6b29 --- /dev/null +++ b/src/generators/toc.ts @@ -0,0 +1,62 @@ +import { readFile } from "node:fs/promises"; +import { md } from "mdbox"; +import { initMdAstParser } from "mdbox/parser"; +import type { ParsedTree } from "mdbox/parser"; +import { fileURLToPath } from "mlly"; +import { defineGenerator } from "../generator"; +import { slug } from "github-slugger"; + +function getTextContentFromAst(tree: ParsedTree): string { + let content = ""; + for (const node of tree) { + if (typeof node === "string") { + content += node; + continue; + } + content += getTextContentFromAst(node.children ?? []); + } + return content; +} + +export const toc = defineGenerator({ + name: "toc", + async generate({ args, url }) { + const minLevel: number = Number.parseInt(args.minLevel ?? 2); + const maxLevel: number = Number.parseInt(args.maxLevel ?? 3); + + if (url === undefined) { + throw new Error("URL is required for toc generator"); + } + + const contents = await readFile(fileURLToPath(url), "utf8"); + const parser = await initMdAstParser(); + const { tree } = parser.parse(contents); + + const toc = []; + const allowedNodeTypes = new Set( + ["h1", "h2", "h3", "h4", "h5", "h6"].slice(minLevel - 1, maxLevel), + ); + for (const node of tree) { + if (typeof node === "string") { + continue; + } + if (allowedNodeTypes.has(node.type)) { + toc.push({ + level: Number.parseInt(node.type.slice(1)) - minLevel, + text: getTextContentFromAst(node.children ?? []), + }); + } + } + + const tocMd = toc + .map(({ level, text }) => { + const content = md.link(`#${slug(text)}`, text); + return `${" ".repeat(level)}- ${content}`; + }) + .join("\n"); + + return { + contents: tocMd, + }; + }, +}); diff --git a/test/fixture/INPUT.md b/test/fixture/INPUT.md index ac559c7..c58bddb 100644 --- a/test/fixture/INPUT.md +++ b/test/fixture/INPUT.md @@ -1,5 +1,30 @@ # Automd built-in generator fixtures +## Table of Contents + + + + +## Heading test + +### sub heading 1 + +#### sub sub heading + +### sub heading 2 + +#### sub sub heading 1 + +#### sub sub heading 2 + +##### do you really need this many headings? + +## heading with [link](#) + +## heading with footnote[^1] + +[^1]: this is a footnote + ## `badges` diff --git a/test/fixture/OUTPUT.md b/test/fixture/OUTPUT.md index 7b661d9..76bccfe 100644 --- a/test/fixture/OUTPUT.md +++ b/test/fixture/OUTPUT.md @@ -1,5 +1,50 @@ # Automd built-in generator fixtures +## Table of Contents + + + +- [Table of Contents](#table-of-contents) +- [Heading test](#heading-test) + - [sub heading 1](#sub-heading-1) + - [sub sub heading](#sub-sub-heading) + - [sub heading 2](#sub-heading-2) + - [sub sub heading 1](#sub-sub-heading-1) + - [sub sub heading 2](#sub-sub-heading-2) +- [heading with link](#heading-with-link) +- [heading with footnote](#heading-with-footnote) +- [badges](#badges) +- [pm-x](#pm-x) +- [pm-install](#pm-install) +- [jsdocs](#jsdocs) +- [jsimport](#jsimport) +- [with-automd](#with-automd) +- [fetch](#fetch) +- [file](#file) +- [contributors](#contributors) + + + +## Heading test + +### sub heading 1 + +#### sub sub heading + +### sub heading 2 + +#### sub sub heading 1 + +#### sub sub heading 2 + +##### do you really need this many headings? + +## heading with [link](#) + +## heading with footnote[^1] + +[^1]: this is a footnote + ## `badges`