94 lines
2.1 KiB
TypeScript
94 lines
2.1 KiB
TypeScript
import type { Node } from "@markdoc/markdoc";
|
|
|
|
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
|
|
|
interface HeadingNode extends Node {
|
|
type: "heading";
|
|
attributes: {
|
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
id?: string;
|
|
[key: string]: unknown;
|
|
};
|
|
}
|
|
|
|
type H2Node = HeadingNode & {
|
|
attributes: {
|
|
level: 2;
|
|
};
|
|
};
|
|
|
|
type H3Node = HeadingNode & {
|
|
attributes: {
|
|
level: 3;
|
|
};
|
|
};
|
|
|
|
function isHeadingNode(node: Node): node is HeadingNode {
|
|
return (
|
|
node.type === "heading" &&
|
|
[1, 2, 3, 4, 5, 6].includes(node.attributes.level) &&
|
|
(typeof node.attributes.id === "string" || typeof node.attributes.id === "undefined")
|
|
);
|
|
}
|
|
|
|
function isH2Node(node: Node): node is H2Node {
|
|
return isHeadingNode(node) && node.attributes.level === 2;
|
|
}
|
|
|
|
function isH3Node(node: Node): node is H3Node {
|
|
return isHeadingNode(node) && node.attributes.level === 3;
|
|
}
|
|
|
|
function getNodeText(node: Node) {
|
|
let text = "";
|
|
for (let child of node.children ?? []) {
|
|
if (child.type === "text") {
|
|
text += child.attributes.content;
|
|
}
|
|
text += getNodeText(child);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
export type Subsection = H3Node["attributes"] & {
|
|
id: string;
|
|
title: string;
|
|
children?: undefined;
|
|
};
|
|
|
|
export type Section = H2Node["attributes"] & {
|
|
id: string;
|
|
title: string;
|
|
children: Array<Subsection>;
|
|
};
|
|
|
|
export function collectSections(nodes: Array<Node>, slugify = slugifyWithCounter()) {
|
|
const sections: Array<Section> = [];
|
|
|
|
for (const node of nodes) {
|
|
if (isH2Node(node) || isH3Node(node)) {
|
|
const title = getNodeText(node);
|
|
if (title) {
|
|
const id = slugify(title);
|
|
|
|
if (isH3Node(node)) {
|
|
if (!sections[sections.length - 1]) {
|
|
throw new Error("Cannot add `h3` to table of contents without a preceding `h2`");
|
|
}
|
|
sections[sections.length - 1].children.push({
|
|
...node.attributes,
|
|
id,
|
|
title,
|
|
});
|
|
} else {
|
|
sections.push({ ...node.attributes, id, title, children: [] });
|
|
}
|
|
}
|
|
}
|
|
|
|
sections.push(...collectSections(node.children ?? [], slugify));
|
|
}
|
|
|
|
return sections;
|
|
}
|