import { CONTINUE, SKIP, visit } from 'unist-util-visit'; import { fromHtmlIsomorphic } from 'hast-util-from-html-isomorphic'; import { toString } from 'hast-util-to-string'; import { isElement } from 'hast-util-is-element'; import { matches } from 'hast-util-select'; // Add IDs and SVG permalinks to headings // (rehype-autolink-headings is good, but can't be configured to ignore some headings) const anchorHtml = ''; // Should match the method in bin/build/checks/links.pl function slugIt(heading) { return toString(heading) .trim() .toLowerCase() .replace(/\s+/g, '-') .replace(/[^a-z0-9_-]/g, ''); } function autoLinkHeadings(options) { const { headings, exclude } = options; return function (tree) { visit(tree, 'element', (node) => { if (!isElement(node, headings) || (exclude && matches(exclude, node))) { return CONTINUE; } if (!node.properties.id) { node.properties.id = slugIt(node); } const id = node.properties.id; const anchor = fromHtmlIsomorphic(anchorHtml, { fragment: true }) .children[0]; anchor.properties = { ...anchor.properties, href: '#' + id }; node.children.unshift(anchor); return SKIP; }); }; } // The headings to process const defaultHeadings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; // Headings that match this selector are ignored const defaultExclude = undefined; export default function (options) { const headings = options?.headings ?? defaultHeadings; const exclude = options?.exclude ?? defaultExclude; return { name: 'myAutoLinkHeadings', hooks: { 'astro:config:setup': ({ updateConfig, logger }) => { logger.debug('Headings: ' + headings); logger.debug('Exclude: ' + exclude); updateConfig({ markdown: { rehypePlugins: [ [autoLinkHeadings, { headings: headings, exclude: exclude }], ], }, }); }, }, }; }