*/ public static function extractToc(string $content): array { $items = []; $lines = explode("\n", $content); // ── DEBUG: temporary TOC diagnostics ────────────────────────── error_log('[TOC-DEBUG] content length: ' . strlen($content)); error_log('[TOC-DEBUG] line count: ' . count($lines)); foreach (array_slice($lines, 0, 8) as $i => $line) { if ($line === '') { error_log('[TOC-DEBUG] line[' . $i . '] (empty)'); } else { $hex = bin2hex($line); $preview = mb_substr($line, 0, 60); error_log('[TOC-DEBUG] line[' . $i . '] hex=' . $hex . ' raw=' . $preview); } } // ── END DEBUG ───────────────────────────────────────────────── // Use CommonMark's own SlugNormalizer so TOC links match the rendered heading IDs exactly. $normalizer = new \League\CommonMark\Normalizer\SlugNormalizer(); foreach ($lines as $line) { if (preg_match('/^(#{1,3})\s+(.+)$/', $line, $m)) { $level = strlen($m[1]); $label = trim($m[2]); $id = $normalizer->normalize($label); $items[] = ['label' => $label, 'href' => '#' . $id, 'level' => $level]; error_log('[TOC-DEBUG] MATCH: h' . $level . ' "' . $label . '" -> #' . $id); } } error_log('[TOC-DEBUG] total items found: ' . count($items)); return $items; } }