Visibility internal Owner _ Approver _ Created _ Updated _
generate-toc.ts
| Field | Value |
|---|---|
| Type | TypeScript |
| Source | Product/Projects/Uvilo_Method/Skills/Uvilo_Method_Release/scripts/generate-toc.ts |
| Parent | Product |
| GitHub | Product/Projects/Uvilo_Method/Skills/Uvilo_Method_Release/scripts/generate-toc.ts |
This page is auto-generated. Edit the source to change the content.
#!/usr/bin/env npx tsx
/**
* Generate a TOC for H1/H2 headings and replace \[\[Table of Contents]] placeholder.
*
* Supports two anchor styles:
* --style github (default) — GitHub-flavor anchors (keeps leading numbers)
* --style pandoc — Pandoc-compatible anchors (strips leading numbers/dots)
*/
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
// ── Argument Parsing ──────────────────────────────────────────────────────────
interface Args {
mdFile: string;
style: "github" | "pandoc";
skipFirst: boolean;
}
function parseArgs(): Args {
const argv = process.argv.slice(2);
let style: "github" | "pandoc" = "github";
let skipFirst = false;
let mdFile = "";
for (let i = 0; i < argv.length; i++) {
if (argv[i] === "--style" && argv[i + 1]) {
style = argv[i + 1] as "github" | "pandoc";
i++;
} else if (argv[i] === "--skip-first") {
skipFirst = true;
} else if (!argv[i].startsWith("--")) {
mdFile = argv[i];
}
}
if (!mdFile) {
console.error("Usage: npx tsx generate-toc.ts <md_file> [--style github|pandoc] [--skip-first]");
process.exit(1);
}
return { mdFile, style, skipFirst };
}
// ── Anchor Generation ─────────────────────────────────────────────────────────
function githubAnchor(title: string): string {
/** Generate an anchor ID matching CommonMark/GitHub-flavored markdown.
*
* Preserves Unicode characters (like °) while removing common punctuation.
* This matches the behavior of modern markdown viewers like Cursor.
*/
let s = title.toLowerCase();
// Remove only specific punctuation characters (not all non-word chars)
// Keep: letters (including Unicode), digits, spaces, hyphens, and special symbols like °
// Remove: periods, commas, colons, semicolons, quotes, brackets, parentheses, etc.
s = s.replace(/[.,;:!?'"()\[\]{}]/g, "");
// Replace whitespace with hyphens
s = s.replace(/\s+/g, "-");
// Collapse multiple hyphens
s = s.replace(/-+/g, "-");
s = s.replace(/^-+|-+$/g, "");
return s;
}
function pandocAnchor(title: string): string {
/** Generate an anchor ID matching pandoc's algorithm. */
let s = title;
// Strip leading numbering: "1. ", "1.1 ", "4.2. ", "11.8. " etc.
s = s.replace(/^[\d]+(?:\.[\d]+)*\.?\s+/, "");
return githubAnchor(s);
}
// ── Main ───────────────────────────────────────────────────────────────────────
const args = parseArgs();
const anchorFn = args.style === "github" ? githubAnchor : pandocAnchor;
let content: string;
try {
content = readFileSync(args.mdFile, "utf-8");
} catch (e: any) {
console.error(`ERROR: Cannot read file: ${args.mdFile} — ${e.message}`);
process.exit(1);
}
// Extract H1 and H2 headings, skipping code blocks
const lines = content.split("\n");
const tocLines: string[] = [];
let inCode = false;
let firstHeadingSeen = false;
for (const line of lines) {
if (line.startsWith("```")) {
inCode = !inCode;
continue;
}
if (inCode) continue;
const m = line.match(/^(#{1,2})\s+(.+)$/);
if (m) {
const level = m[1].length;
const title = m[2].trim();
if (!firstHeadingSeen) {
firstHeadingSeen = true;
if (args.skipFirst) continue;
}
if (title.toLowerCase() === "table of contents") continue;
const anchor = anchorFn(title);
const indent = level === 2 ? " " : "";
tocLines.push(`${indent}- [${title}](#${anchor})`);
}
}
const toc = tocLines.join("\n");
const placeholder = "\\[\\[Table of Contents]]";
if (content.includes(placeholder)) {
content = content.replace(placeholder, toc);
writeFileSync(args.mdFile, content);
console.log(` TOC inserted (${tocLines.length} entries, style=${args.style})`);
} else {
console.log(` ⚠️ No \\[\\[Table of Contents]] placeholder found — skipping`);
}