slugify
Heading-anchor slug utilities — slugify() and SlugAllocator — exported from the @takazudo/zfb/slugify subpath.
Signature
function slugify(input: string): stringExported from the @takazudo/ subpath. Produces the id attribute value zfb assigns to <h2>–<h6> heading elements, processing input one Unicode code point at a time: whitespace and a fixed ASCII punctuation set (! " # $ % & ' ( ) * + , . / : ; < = > ? @ [ \ ] ^ ` { | } ~, plus Unicode control characters) collapse to a single - separator — no leading, trailing, or duplicate dashes — and every other character is lowercased and kept. slugify("Hello, World!") returns "hello-world".
Not github-slugger compatible
slugify is intentionally not a drop-in replacement for npm github-slugger. Stripped punctuation collapses to a - (slugify("a,b") returns "a-b", where github-slugger produces "ab"), and pre-existing dashes/underscores pass through unchanged. It is a byte-for-byte TypeScript port of zfb's own Rust slugify (crates/) — parity is enforced by a shared fixture (crates/) consumed by both a Rust integration test and this package's slugify.test.ts vitest suite.
This produces a heading-anchor slug — distinct from the collection-entry URL slug (CollectionEntry.slug), which is produced by a separate internal helper from a file's path, not its heading text.
SlugAllocator
class SlugAllocator {
constructor(strategy?: "flat" | "hierarchical");
allocate(depth: number, base: string): string;
reset(): void;
}Per-document dedupe allocator layered on top of slugify()'s output, mirroring the Rust SlugAllocator. Construct one per document — or call reset() between documents to clear its state — and call allocate(depth, base) once per heading, in document order:
depth— heading depth,2–6.h1is never allocated; it's owned by the page layout, not the Markdown body.base— theslugify()-ed heading text.
Two strategies, matching Heading links — strategy:
"flat"(default) — a single per-document dedupe counter: the first heading that slugifies to"intro"getsintro, the second getsintro-1, the thirdintro-2, and so on."hierarchical"— each slug is prefixed with its ancestor heading chain (popping ancestors at or above the current depth first), then deduped on the full candidate path — e.g.foo,foo-moo,foo-moo-mewfor aFoo>Moo>Mewheading nest.
import { SlugAllocator } from "@takazudo/zfb/slugify";
const allocator = new SlugAllocator("flat");
allocator.allocate(2, "getting-started"); // "getting-started"
allocator.allocate(2, "getting-started"); // "getting-started-1" (collision)
allocator.reset(); // clear counters before the next documentAn empty base (a heading that slugifies to nothing) returns "" from both allocate and the standalone slugify without mutating allocator state.
See also
Heading links — the always-on Markdown plugin that calls this allocator to assign
idattributes and inject anchor links on every heading.