Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ dist
.eslintcache
*.log*
*.env*
.pnpm-store
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/generators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,4 +20,5 @@ export default {
jsimport,
"with-automd": withAutomd,
contributors,
toc,
} as Record<string, Generator>;
62 changes: 62 additions & 0 deletions src/generators/toc.ts
Original file line number Diff line number Diff line change
@@ -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,
};
},
});
25 changes: 25 additions & 0 deletions test/fixture/INPUT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Automd built-in generator fixtures

## Table of Contents

<!-- automd:toc max-level="4" -->
<!-- /automd -->

## 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`

<!-- automd:badges bundlephobia packagephobia -->
Expand Down
45 changes: 45 additions & 0 deletions test/fixture/OUTPUT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
# Automd built-in generator fixtures

## Table of Contents

<!-- automd:toc max-level="4" -->

- [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)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link is broken. The ast for this node is

[
  {
    "type": "h2",
    "children": [
      "heading with footnote",
      {
        "type": "footnoteReference"
      }
    ]
  },
  {
    "type": "footnoteDefinition",
    "children": [
      {
        "type": "p",
        "children": ["this is a footnote"]
      }
    ]
  }
]

The slug generated by this plugin is #heading-with-footnote while github generates #heading-with-footnote1

- [badges](#badges)
- [pm-x](#pm-x)
- [pm-install](#pm-install)
- [jsdocs](#jsdocs)
- [jsimport](#jsimport)
- [with-automd](#with-automd)
- [fetch](#fetch)
- [file](#file)
- [contributors](#contributors)

<!-- /automd -->

## 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`

<!-- automd:badges bundlephobia packagephobia -->
Expand Down