sass-site/source/helpers/components/toc.ts

86 lines
2.2 KiB
TypeScript
Raw Normal View History

2023-05-25 17:00:51 +02:00
import * as cheerio from 'cheerio';
type TOCItem = {
[key: string]: string | boolean | TOCItem[];
2023-05-25 17:00:51 +02:00
};
/**
* Returns `text` and `href` for a documentation table-of-contents section.
*/
export const getDocTocData = (data: TOCItem) => {
const text = Object.keys(data).filter(
(key) => ![':children', ':expanded'].includes(key),
)[0];
2023-05-25 17:00:51 +02:00
const href = data[text] as string;
const expanded = Boolean(data[':expanded']);
return { text, href, expanded };
2023-05-25 17:00:51 +02:00
};
/**
* Generates table of contents data for a documentation page.
*/
export const getToc = (html: string, topLevelTotal: number): TOCItem[] => {
2023-05-25 17:00:51 +02:00
const $ = cheerio.load(html);
$('a.anchor').remove();
const headings = $('h2, h3, h4, h5, h6').filter('[id]');
if (!headings.length) {
return [];
}
const toc: TOCItem[] = [];
let stack: TOCItem[] = [];
const byLevel: Record<number, TOCItem[]> = {
2: [],
3: [],
4: [],
5: [],
6: [],
};
2023-05-25 17:00:51 +02:00
headings.each((index, element) => {
const h = element as cheerio.TagElement;
const level = parseInt(h.name[1], 10);
const title = $(h).html() as string;
const id = $(h).attr('id') as string;
const tocItem: TOCItem = { [title]: `#${id}` };
byLevel[level].push(tocItem);
2023-05-25 17:00:51 +02:00
if (level === 2) {
toc.push(tocItem);
stack = [tocItem];
} else {
const parent = stack[level - 3];
if (parent) {
if (!parent[':children']) {
parent[':children'] = [];
}
(parent[':children'] as TOCItem[]).push(tocItem);
stack.length = level - 2;
stack[level - 2] = tocItem;
} else {
throw new Error(`Invalid heading level without parent: h${level}`);
}
}
});
// Expand the table of contents to the deepest level possible without making
// it longer than the most-collapsed-possible top-level documentation table of
// contents.
2023-05-25 21:02:28 +02:00
let expandedLevel = 3;
let totalEntries = byLevel[2].length;
while (expandedLevel < 7) {
2023-05-25 21:02:28 +02:00
const children = byLevel[expandedLevel];
totalEntries += children.length;
2023-05-25 21:02:28 +02:00
if (totalEntries > topLevelTotal) {
break;
}
2023-05-25 21:02:28 +02:00
for (const entry of byLevel[expandedLevel - 1]) {
entry[':expanded'] = true;
}
expandedLevel += 1;
}
2023-05-25 17:00:51 +02:00
return toc;
};