Skip to content
Visibility internal Owner _ Approver _ Created _ Updated _

generate-toc.ts



FieldValue
TypeTypeScript
SourceProduct/Projects/Uvilo_Method/Skills/Uvilo_Method_Release/scripts/generate-toc.ts
ParentProduct
GitHubProduct/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`);
}