Skip to content

Instantly share code, notes, and snippets.

@nasrulhazim
Created October 30, 2025 12:52
Show Gist options
  • Save nasrulhazim/25178ae097322f3f5a488ad271abe87a to your computer and use it in GitHub Desktop.
Save nasrulhazim/25178ae097322f3f5a488ad271abe87a to your computer and use it in GitHub Desktop.
Generate TOC for Markdown

Ensure there's folder docs/ in root directory. Then:

  1. Copy paste the code generate-toc.php.
  2. Run script php generate-toc.php
#!/usr/bin/env php
<?php
/**
* Generate Table of Contents for Documentation
*
* Usage: php generate-toc.php [path]
* Example: php generate-toc.php docs/
*/
class TocGenerator
{
protected array $ignoredFiles = ['README.md'];
protected array $ignoredFolders = ['.git', 'vendor', 'node_modules'];
protected int $maxDepth = 5;
public function generate(string $path, int $currentDepth = 0): string
{
if ($currentDepth > $this->maxDepth) {
return '';
}
$path = rtrim($path, '/');
if (!is_dir($path)) {
echo "Error: Path '{$path}' is not a directory.\n";
exit(1);
}
$items = $this->scanDirectory($path);
$output = '';
foreach ($items as $item) {
$fullPath = $path . '/' . $item;
if (is_dir($fullPath)) {
// Skip ignored folders
if (in_array($item, $this->ignoredFolders)) {
continue;
}
// Add folder header
$indent = str_repeat(' ', $currentDepth);
$folderName = $this->formatName($item);
$sectionNumber = $this->extractSectionNumber($item);
if ($sectionNumber) {
$output .= "{$indent}- **{$sectionNumber}. {$folderName}**\n";
} else {
$output .= "{$indent}- **{$folderName}**\n";
}
// Recursively process subfolder
$subContent = $this->generate($fullPath, $currentDepth + 1);
if ($subContent) {
$output .= $subContent;
}
} elseif (is_file($fullPath) && pathinfo($fullPath, PATHINFO_EXTENSION) === 'md') {
// Skip ignored files
if (in_array($item, $this->ignoredFiles)) {
continue;
}
// Add markdown file
$indent = str_repeat(' ', $currentDepth);
$fileName = $this->formatName(pathinfo($item, PATHINFO_FILENAME));
$relativePath = str_replace(getcwd() . '/', '', $fullPath);
$output .= "{$indent} - [{$fileName}]({$relativePath})\n";
}
}
return $output;
}
protected function scanDirectory(string $path): array
{
$items = scandir($path);
// Remove . and ..
$items = array_diff($items, ['.', '..']);
// Sort: folders first, then files, both alphabetically
usort($items, function($a, $b) use ($path) {
$aIsDir = is_dir($path . '/' . $a);
$bIsDir = is_dir($path . '/' . $b);
if ($aIsDir && !$bIsDir) {
return -1;
}
if (!$aIsDir && $bIsDir) {
return 1;
}
return strnatcasecmp($a, $b);
});
return $items;
}
protected function formatName(string $name): string
{
// Remove section number prefix (e.g., "01-getting-started" -> "Getting Started")
$name = preg_replace('/^\d+-/', '', $name);
// Replace hyphens and underscores with spaces
$name = str_replace(['-', '_'], ' ', $name);
// Capitalize words
return ucwords($name);
}
protected function extractSectionNumber(string $name): ?string
{
if (preg_match('/^(\d+)-/', $name, $matches)) {
return ltrim($matches[1], '0') ?: '0';
}
return null;
}
public function generateWithHeader(string $path): string
{
$toc = "## πŸ“š Documentation Structure\n\n";
$toc .= "This documentation is organized into the following sections:\n\n";
$toc .= $this->generate($path);
return $toc;
}
}
// Main execution
$path = $argv[1] ?? 'docs';
if (!file_exists($path)) {
echo "Error: Path '{$path}' does not exist.\n";
echo "Usage: php generate-toc.php [path]\n";
echo "Example: php generate-toc.php docs/\n";
exit(1);
}
$generator = new TocGenerator();
$toc = $generator->generateWithHeader($path);
echo $toc;
echo "\n";
echo "βœ… Table of Contents generated successfully!\n";
echo "πŸ“‹ Copy the output above and paste it into your README.md\n";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment