Generating a table of contents with collapsible subsections, PhP or JQuery? 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}&lt;h1&gt;A1&lt;/h1&gt;a1&lt;h1&gt;A2&lt;/h1&gt;a2 &lt;h2&gt;B1&lt;/h2&gt;b1&lt;h2&gt;B2&lt;/h2&gt;b2 &lt;h3&gt;C1&lt;/h3&gt;c1&lt;h3&gt;C2&lt;/h3&gt;c2  &lt;h1&gt;D1&lt;/h1&gt;d1 &lt;h2&gt;E1&lt;/h2&gt;e1 &lt;h3&gt;F1&lt;/h3&gt;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('#&lt;(h[1-9])(.*?)&gt;(.*?)&lt;/\1&gt;#si', function ($matches) use (&amp;$index, &amp;$tableOfContents, &amp;$old_level, &amp;$id_title, &amp;$toc_title, &amp;$maybe_button) {
        $tag = $matches[1];
        $toc_entry_title = strip_tags($matches[3]);
        $hasId = preg_match('#id=("|”)(.*?)\1[\s&gt;]#si', $matches[2], $matchedIds);
        $id = $hasId ? $matchedIds[2] : ($id_title . '-' . $index++ . '-' . /*sanitize_title*/($toc_entry_title));

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

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

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

        $old_level = $new_level;

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

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

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


JQuery code:

$('.toc_button').click(function() {
  if ($(this).val() === "-") {
    $(this).val("+");
  } else {
    $(this).val("-");
  }
  $('#div_' + this.id).toggle();
});
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 &lt;/div&gt; in at the end.