docusaurus-numbered-headings
Version:
A Docusaurus plugin that automatically adds numbered headings with support for ISO 2145 and USA Classic numbering conventions
86 lines (76 loc) • 3 kB
text/typescript
/**
* Remark plugin that drives per-document numbered-heading behavior via frontmatter.
*
* Wire it into your Docusaurus docs preset:
*
* const { remarkFrontmatterToggle } = require("docusaurus-numbered-headings");
* // ...
* docs: { remarkPlugins: [remarkFrontmatterToggle], ... }
*
* Then in any MDX file, control numbering with the `numbered_headings` frontmatter
* key:
*
* ---
* numbered_headings: false # disable auto-numbering on this page
* numbered_headings: "iso-2145" # force ISO 2145 (1, 1.1, 1.1.1)
* numbered_headings: "usa-classic" # force USA Classic (I, A, 1, a)
* numbered_headings: "spanish-forense" # force Spanish forensic (I, Primero.-, 1, a)
* numbered_headings: true # explicit default (same as unset)
* ---
*
* When set, the plugin wraps the document body in a `<div className="…">` carrying:
* - `disable_numbered_headings` for `false`
* - `numbered_headings_iso_2145` for `"iso-2145"`
* - `numbered_headings_usa_classic` for `"usa-classic"`
* - `numbered_headings_spanish_forense` for `"spanish-forense"`
*
* Plugin CSS recognizes those classes and applies (or suppresses) the right
* counters. The right-rail Table of Contents is scoped via `:root:has(...)`
* since it lives outside the wrapped document body.
*
* Compatible with Docusaurus 3+, which exposes parsed frontmatter on
* `file.data.frontMatter`.
*/
type AnyNode = { type: string; [key: string]: unknown };
type Root = { type: "root"; children: AnyNode[] };
type VFileLike = { data?: { frontMatter?: Record<string, unknown> } };
const TOP_LEVEL_PRESERVE = new Set(["mdxjsEsm", "yaml", "toml"]);
function classForFrontmatter(value: unknown): string | null {
if (value === false) return "disable_numbered_headings";
if (value === "iso-2145") return "numbered_headings_iso_2145";
if (value === "usa-classic") return "numbered_headings_usa_classic";
if (value === "spanish-forense") return "numbered_headings_spanish_forense";
// `true`, `undefined`, or anything else: no override, use the global default.
return null;
}
export function remarkFrontmatterToggle() {
return (tree: Root, file: VFileLike): void => {
const fm = file?.data?.frontMatter;
if (!fm) return;
const wrapperClass = classForFrontmatter(fm.numbered_headings);
if (!wrapperClass) return;
const preserved: AnyNode[] = [];
const wrapped: AnyNode[] = [];
for (const child of tree.children) {
if (TOP_LEVEL_PRESERVE.has(child.type)) {
preserved.push(child);
} else {
wrapped.push(child);
}
}
const wrapper: AnyNode = {
type: "mdxJsxFlowElement",
name: "div",
attributes: [
{
type: "mdxJsxAttribute",
name: "className",
value: wrapperClass,
},
],
children: wrapped,
};
tree.children = [...preserved, wrapper];
};
}
export default remarkFrontmatterToggle;