The code below generates tables of contents with collapsible subsections from HTML headers. I have done it for a WordPress website, so I can use either PHP or JQuery/JavaScript. I have used PHP to generate the HTML of the ToC and JQuery to collapse/un-collapse the sections.

The code works fine, but I was wondering if it could be simpler/more robust.

PHP fiddle is here: https://paiza.io/projects/fyq6rNEs5H6AO77DkDRRXQ . It outputs HTML code that goes in JSFiddle: https://jsfiddle.net/yu51a5/jasngvfz/37/

PHP code:

    $content="  {yu_TOC}<h1>A1</h1>a1<h1>A2</h1>a2 <h2>B1</h2>b1<h2>B2</h2>b2 <h3>C1</h3>c1<h3>C2</h3>c2  <h1>D1</h1>d1 <h2>E1</h2>e1 <h3>F1</h3>f1";

    $start_text = strpos($content, '}') + 1;

    $toc_title = 'Table Of Contents';
    $id_title = /*sanitize_title*/($toc_title);

    $index = 1;
    $tableOfContents = "";
    $old_level = 0;
    $maybe_button = "";
    // Insert the IDs and create the TOC.
    $content = preg_replace_callback('#<(h[1-9])(.*?)>(.*?)</\1>#si', function ($matches) use (&$index, &$tableOfContents, &$old_level, &$id_title, &$toc_title, &$maybe_button) {
        $tag = $matches[1];
        $toc_entry_title = strip_tags($matches[3]);
        $hasId = preg_match('#id=("|”)(.*?)\1[\s>]#si', $matches[2], $matchedIds);
        $id = $hasId ? $matchedIds[2] : ($id_title . '-' . $index++ . '-' . /*sanitize_title*/($toc_entry_title));

        $new_level = intval($tag[1]);

        // <div> is not closed, because we might want to add a button
        $tableOfContents_entry = "<div class='item-$tag' id='toc_item_$id'><a href='#$id'>$toc_entry_title</a>"; 
        if ($new_level > $old_level) { // $new_level = $old_level + 1
            // adding a button + other html from the previous iteration
            $tableOfContents .= $maybe_button . $tableOfContents_entry;
        } else {
            // close the <div>'s
            for ($x = $new_level; $x < $old_level; $x++) {
                $tableOfContents_entry = "</div>" . $tableOfContents_entry;
            $tableOfContents .= "</div>" . $tableOfContents_entry;  

        // creating html in case it is needed at the next iteration
        $maybe_button = '<input class="toc_button" id="button_toc_' . $id . '"  type="button" value="+" /></div><div class="div_toc_initial" id="div_button_toc_' . $id . '">';

        $old_level = $new_level;

        return "<$tag $matches[2] id='$id'>$matches[3]</$tag><a href='#toc_item_$id'>↑ Back To $toc_title ↑</a>";
     }, $content);

    // make sure all divs are closed
    $nb_opening_divs = preg_match_all('/<div(.*?)>/i', $tableOfContents, $matches);
    $nb_closing_divs = preg_match_all('/<\/div>/i', $tableOfContents, $matches);
    for ($x = $nb_closing_divs; $x < $nb_opening_divs; $x++) {
        $tableOfContents .= "</div>";

    $result = "<h1><a id='$id_title'>$toc_title</a></h1>" . '<div>' . $tableOfContents . '</div>' . trim(substr($content, $start_text)); 
    echo $result;

JQuery code:

$('.toc_button').click(function() {
  if ($(this).val() === "-") {
  } else {
  $('#div_' + this.id).toggle();

1 Answer 1


I find your php script incredibly hard to read. Maintaining this script would surely involve substantial time investment.

You are conducting far too much preg_ surgery on input that can be prepared to be valid html. Processing html is far more reliable via a proper DOM parsing tool. My preferred tool is DOMDocument and I often pair it with XPath when it makes node targeting clearer/simpler.

I don't have the time to completely rewrite your script, but that is precisely what I recommend.

You shouldn't ever need to perform "mopping up" of your generated html by throwing some more </div> in at the end.

  • \$\begingroup\$ Fair enough, thank you for your feedback. In my defence, preg_ parsing is read-only except I am replacing '{yu_TOC}' with HTML code for the table of contents. I can count precisely how many </div>'s I can add at the end in the loop, but this will make the loop more complex, so mopping is for simplicity. Again, many thanks for your recommendations, I will look into DOMDocument. \$\endgroup\$
    – Yulia V
    Dec 21, 2019 at 8:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.