Banner

Table of Content

Add Table of Contents in your blog posts.

PHP
/**
 * Snippet Name:     Table of Contents
 * Snippet Author:   coding-bunny.com
 * Description:      Add Table of Contents in your blog posts.
 * Version:          1.0.0
 * 
 * USAGE EXAMPLES:
 * [cbtoc title="Table of content" levels="h2,h3,h4" toggle_view="yes" initial_view="show" exclude_titles="Introduction,Summary"]
 */

if (!defined('ABSPATH')) exit;

add_shortcode('cbtoc', function($atts) {
    $atts = shortcode_atts([
        'levels'        => 'h2,h3',
        'title'         => 'Table of Contents',
        'toggle_view'   => 'no',
        'initial_view'  => 'show',
        'exclude_titles'=> '',
    ], $atts, 'cbtoc');

    $allowed_levels = ['h1','h2','h3','h4','h5','h6'];
    $requested_levels = array_map('trim', explode(',', strtolower($atts['levels'])));
    $levels = array_intersect($allowed_levels, $requested_levels);
    $exclude_titles = array_filter(array_map('trim', explode(',', $atts['exclude_titles'])));
    $exclude_titles_lc = array_map('mb_strtolower', $exclude_titles);

    if (empty($levels)) {
        return '<div class="cbtoc-error">No valid heading level specified for the Table of Contents.</div>';
    }

    global $post;
    if (empty($post) || empty($post->post_content)) return '';

    $content = $post->post_content;
    $headings = [];
    $pattern = '/<(' . implode('|', $levels) . ')([^>]*)>(.*?)<\/\1>/i';

    preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

    if (empty($matches)) {
        return '<div class="cbtoc-empty">No headings found.</div>';
    }

    foreach($matches as $match) {
        $tag = strtolower($match[1]);
        $title = strip_tags($match[3]);
        if (in_array(mb_strtolower(trim($title)), $exclude_titles_lc, true)) {
            continue;
        }
        $slug = sanitize_title($title);
        $level = intval(substr($tag, 1));
        if (!preg_match('/id=["\'](.+?)["\']/', $match[2], $id_match)) {
            $anchor = $slug;
            $content = preg_replace(
                '/<' . $tag . $match[2] . '>' . preg_quote($match[3], '/') . '<\/' . $tag . '>/i',
                '<' . $tag . $match[2] . ' id="' . esc_attr($anchor) . '">' . $match[3] . '</' . $tag . '>',
                $content, 1
            );
        } else {
            $anchor = esc_attr($id_match[1]);
        }
        $headings[] = [
            'title' => $title,
            'anchor' => $anchor,
            'level' => $level,
        ];
    }

    if (empty($headings)) {
        return '<div class="cbtoc-empty">No headings found.</div>';
    }

    $min_level = min(array_column($headings, 'level'));
    $unique_id = 'cbtoc-' . uniqid();

    $toggle_view  = strtolower($atts['toggle_view']) === 'yes';
    $initial_view = strtolower($atts['initial_view']) === 'hide' ? 'hide' : 'show';
    $show_style   = ($toggle_view && $initial_view === 'hide') ? 'display:none;' : '';

    $toc  = '<nav class="cbtoc-pro" aria-label="Table of Contents" id="' . esc_attr($unique_id) . '">';
    $toc .= '<div class="cbtoc-title-wrap">';
    $toc .= '<span class="cbtoc-title">' . esc_html($atts['title']) . '</span>';
    if ($toggle_view) {
        $toggle_label = ($initial_view === 'hide') ? '+' : '';
        $toc .= '<button type="button" class="cbtoc-toggle-btn" aria-expanded="' . (($initial_view==='show')?'true':'false') . '" aria-controls="' . esc_attr($unique_id) . '-list">' . esc_html($toggle_label) . '</button>';
    }
    $toc .= '</div>';
    $toc .= '<div class="cbtoc-contents" id="' . esc_attr($unique_id) . '-list" style="' . $show_style . '">';
    $toc .= cbtoc_build_nested_ul($headings, $min_level);
    $toc .= '</div>';
    $toc .= '</nav>';

    $toc .= '<style>
.cbtoc-pro {
    background: #f9f9fc;
    border: 1px solid #e0e5ee;
    border-radius: 4px;
    padding: 20px;
    margin-bottom: 20px;
    max-width: 100%;
}
.cbtoc-pro .cbtoc-title-wrap {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.cbtoc-pro .cbtoc-title {
    font-size: 16px;
    font-weight: bold;
    color: #142520;
    border-left: 4px solid #177efa;
    padding-left: 10px;
}
.cbtoc-pro .cbtoc-toggle-btn {
    margin-left: 10px;
    border: none;
    background: none;
    font-size: 20px;
    font-weight: bold;
    color: #177efa;
    cursor: pointer;
    line-height: 1;
    padding: 0 3px 0 0;
    align-self: flex-end;
}
.cbtoc-pro .cbtoc-item {
    margin: 5px 0;
    font-size: 14px;
}
.cbtoc-pro .cbtoc-level-2 { margin-left: 0px; font-weight: bold;}
.cbtoc-pro .cbtoc-level-3 { margin-left: 10px; font-weight: normal;}
.cbtoc-pro .cbtoc-level-4 { margin-left: 15px;}
.cbtoc-pro .cbtoc-level-5 { margin-left: 20px;}
.cbtoc-pro .cbtoc-level-6 { margin-left: 25px;}
.cbtoc-pro a {
    color: #142520;
    text-decoration: none;
}
.cbtoc-pro a:hover {
    color: #177efa;
}
.cbtoc-list,
.cbtoc-list ul {
    padding-left: 20px;
    list-style-type: disc !important;
    margin-left: 0;
}
</style>
';
    if ($toggle_view) {
        $toc .= '<script>
(function(){
    var tocNav = document.getElementById("' . esc_js($unique_id) . '");
    if (!tocNav) return;
    var btn = tocNav.querySelector(".cbtoc-toggle-btn");
    var contents = tocNav.querySelector(".cbtoc-contents");
    if (!btn || !contents) return;
    btn.addEventListener("click", function() {
        var isOpen = contents.style.display !== "none";
        contents.style.display = isOpen ? "none" : "";
        btn.textContent = isOpen ? "+" : "–";
        btn.setAttribute("aria-expanded", (!isOpen).toString());
    });
})();
</script>';
    }

    return $toc;
});

function cbtoc_build_nested_ul($headings, $base_level) {
    $toc = '<ul class="cbtoc-list">';
    $prev_level = $base_level;
    $open_uls = 0;

    foreach ($headings as $heading) {
        $curr_level = $heading['level'];
        while ($curr_level > $prev_level) {
            $toc .= '<ul class="cbtoc-list">';
            $open_uls++;
            $prev_level++;
        }
        while ($curr_level < $prev_level) {
            $toc .= '</ul>';
            $open_uls--;
            $prev_level--;
        }
        $toc .= '<li class="cbtoc-item cbtoc-level-' . $curr_level . '"><a href="#' . esc_attr($heading['anchor']) . '">' . esc_html($heading['title']) . '</a></li>';
    }
    while ($open_uls > 0) {
        $toc .= '</ul>';
        $open_uls--;
    }
    $toc .= '</ul>';
    return $toc;
}

How To Implement This Solution?

Leave a Reply

My Agile Privacy
This site uses technical and profiling cookies. You can accept, decline or customize cookies by pressing the desired buttons. By closing this policy you will continue without accepting.

Need help?

Choose one of the following options:

Powered by CodingBunny