You are here

tableofcontents.module in Table of Contents 5

This is a module to generate a table of contents section based on <h[2-3]> tags. It currently depends on the headinganchors.module for hotlinking to work properly. I need to learn how to properly create a module dependancy or redesign to put both filters in the same module.

For an example, see http://www.csaonline.ca/clublist.

File

tableofcontents.module
View source
<?php

/**
 * @file
 * This is a module to generate a table of contents section based on <h[2-3]>
 * tags. It currently depends on the headinganchors.module for hotlinking to work properly.
 * I need to learn how to properly create a module dependancy or redesign to put both filters
 * in the same module.
 *
 * For an example, see http://www.csaonline.ca/clublist.
 */

/**
 * Implementation of hook_help().
 */
function tableofcontents_help($section) {
  switch ($section) {
    case 'admin/modules#description':

      // This description is shown in the listing at admin/modules.
      return t('A module to create a table of contents based on HTML header tags.');
  }
}

/**
 * Implementation of hook_filter_tips().
 */
function tableofcontents_filter_tips($format, $long = FALSE) {
  if ($long) {
    return t('Every instance of "&lt;!--tableofcontents--&gt;" in the input text will be replaced with a table of contents.');
  }
  else {
    return t('Insert &lt;!--tableofcontents--&gt; to insert a dynamic TOC.');
  }
}

/**
 * Implementation of hook_filter().
 *
 * This is where we do the building of the TOC and replace the toc tags as needed.
 */
function tableofcontents_filter($op, $delta = 0, $format = -1, $text = '') {
  if ($op == 'list') {
    return array(
      0 => t('Table of Contents'),
    );
  }

  // This is where we store TOC options set by the user
  global $toc_options;
  switch ($op) {
    case 'description':
      return t('Inserts a table of contents in the place of &lt;!--tableofcontents--&gt; tags.');

    // Ensure that the data is regenerated on every preview
    case 'no cache':
      return TRUE;

    // We'll use the bytes 0xFE and 0xFF to replace < and > here. These bytes
    // are not valid in UTF-8 data and thus unlikely to cause problems.
    case 'prepare':
      $options_regex = "(( [A-z]+: [A-z0-9 ]+;)+)?";
      preg_match_all('!<\\!--tableofcontents' . $options_regex . '-->!', $text, $options_str, PREG_PATTERN_ORDER);
      preg_match_all('/([A-z]+): ([A-z0-9 ]+);/', $options_str[1][0], $options, PREG_PATTERN_ORDER);
      $toc_options = array();
      for ($i = 0; $i < sizeof($options[1]); $i++) {
        $toc_options[$options[1][$i]] = $options[2][$i];
      }

      /**
       * Allowed options
       */
      $allowed_options = array(
        "title",
        "list",
        "minlevel",
        "maxlevel",
      );

      // Check for invalid options
      foreach ($toc_options as $key => $value) {
        if (!in_array($key, $allowed_options)) {
          form_set_error("Table of Contents", t("%key is an invalid option.", array(
            "%key" => $key,
          )));
        }
      }

      // Process default options
      if (!array_key_exists("title", $toc_options)) {
        $toc_options["title"] = "Table of Contents";
      }

      // Translate ToC
      $toc_options["title"] = t($toc_options["title"]);
      if (!array_key_exists("list", $toc_options)) {
        $toc_options["list"] = "ol";
      }
      if (!array_key_exists("minlevel", $toc_options)) {
        $toc_options["minlevel"] = "2";
      }
      if (!array_key_exists("maxlevel", $toc_options)) {
        $toc_options["maxlevel"] = "3";
      }

      // Process allowed options
      if ($toc_options["list"] != "ol" && $toc_options["list"] != "ul") {
        form_set_error("Table of Contents", t("%key is an invalid list option. Please choose 'ol' or 'ul'.", array(
          "%key" => $toc_options["list"],
        )));
      }
      if (!($toc_options["minlevel"] >= 1 && $toc_options["minlevel"] <= 5)) {
        form_set_error("Table of Contents", t("%key is an invalid minimum level option. Please choose a number between 1 and 5.", array(
          "%key" => $toc_options["minlevel"],
        )));
      }
      if (!($toc_options["maxlevel"] >= $toc_options["minlevel"] && $toc_options["maxlevel"] <= 5)) {
        form_set_error("Table of Contents", t("%key is an invalid maximum depth option. Please choose a number between %min and 5.", array(
          "%min" => $toc_options["minlevel"],
          "%key" => $toc_options["maxlevel"],
        )));
      }
      return preg_replace('!<\\!--tableofcontents' . $options_regex . '-->!', '\\xFE!--tableofcontents--\\xFF', $text);
    case 'process':

      // We will build an array which looks like array(array('level' => 1, 'heading' => $text), ...);
      $toc = array();
      $matches = array();

      /*
       * $i is the index of the heading found
       * $matches[0][$i] -> Whole string matched
       * $matches[1][$i] -> First heading level
       * $matches[2][$i] -> Whole string of attributes
       * $matches[3][$i] -> id attibute, used for anchor
       * $matches[4][$i] -> Text of id attribute
       * $matches[5][$i] -> Text inside of h tag
       * $matches[6][$i] -> Close heading level, should be equal to open level
       */
      preg_match_all('/<h([' . $toc_options["minlevel"] . '-' . $toc_options["maxlevel"] . '])( .*(id="([^"]+)" ?.*))?>(.*)<\\/h([' . $toc_options["minlevel"] . '-' . $toc_options["maxlevel"] . '])>/i', $text, $matches, PREG_PATTERN_ORDER);
      $anchors = array();
      for ($i = 0; $i < sizeof($matches[0]); $i++) {

        // Strip HTML and non alphanumerics
        $level = $matches[1][$i];
        $heading = strip_tags($matches[5][$i]);
        $anchor = $matches[4][$i];
        array_push($toc, array(
          'level' => $level,
          'heading' => $heading,
          'anchor' => $anchor,
        ));
      }

      // Build HTML
      // We probably need to find a way to allow styling of this based on the theme.
      // The obvious example is to put a shaded background behind the TOC.
      // In the future it would also be nice to have collapseable headings using javascript
      // for those really long documents.
      $toc_html = "<div class=\"toc\">\n<h" . $toc_options["minlevel"] . ">" . $toc_options["title"] . "</h" . $toc_options["minlevel"] . ">\n<" . $toc_options["list"] . ">\n";
      $depth = $toc_options["minlevel"];
      foreach ($toc as $title) {

        // We need to allow for nested lists
        // It should be trivial to make the heading levels shown in the TOC user customizable
        $curdepth = $title['level'];
        if ($curdepth <= $toc_options["maxlevel"]) {
          if ($curdepth > $depth) {
            $toc_html .= "<" . $toc_options["list"] . ">\n";
          }
          else {
            if ($curdepth < $depth) {
              $toc_html .= "</" . $toc_options["list"] . ">\n";
            }
          }
          $depth = $curdepth;

          // Insert the list element.
          $toc_html .= "<li><a href=\"#" . $title['anchor'] . "\">" . $title['heading'] . "</a></li>\n";
        }
      }

      // Did we recurse back out to h2 tags? If not, close open lists.
      while ($depth > $toc_options["minlevel"]) {
        $toc_html .= "</" . $toc_options["list"] . ">\n";
        $depth = $depth - 1;
      }
      $toc_html .= "</div>\n";

      // Find our previously changed string and replace with our generated TOC.
      return str_replace('\\xFE!--tableofcontents--\\xFF', $toc_html, $text);
  }
}

Functions

Namesort descending Description
tableofcontents_filter Implementation of hook_filter().
tableofcontents_filter_tips Implementation of hook_filter_tips().
tableofcontents_help Implementation of hook_help().