You are here

tableofcontents.module in Table of Contents 7

This is a filter module to generate a collapsible jquery enabled mediawiki style table of contents based on <h[1-6]> tags. Transforms header tags into named anchors.

It is a complete rewrite of the original non-jquery enabled tableofcontents filter module as follows: +added jquery to make ToC collapsible +preserves attributes on the header tags +checks for existing ID on headers and uses that if found (if none, creates one) +extends the minimum header level to 1 +header conversion is case insensitive +made the regex match for options on the <!--tableofcontents--> marker tolerant of spaces +replaced the comment with [tag ...] +added a more explanatory error message for invalid options & concatenated it into one string to prevent duplicates from being displayed +added several divs to make ToC themable via CSS +provided basic CSS

File

tableofcontents.module
View source
<?php

/**
 * @file
 * This is a filter module to generate a collapsible jquery enabled mediawiki
 * style table of contents based on <h[1-6]> tags. Transforms header tags into
 * named anchors.
 *
 * It is a complete rewrite of the original non-jquery enabled tableofcontents
 * filter module as follows:
 *   +added jquery to make ToC collapsible
 *   +preserves attributes on the header tags
 *   +checks for existing ID on headers and uses that if found (if none,
 *    creates one)
 *   +extends the minimum header level to 1
 *   +header conversion is case insensitive
 *   +made the regex match for options on the <!--tableofcontents-->
 *    marker tolerant of spaces
 *   +replaced the comment with [tag ...]
 *   +added a more explanatory error message for invalid options & concatenated
 *    it into one string to prevent duplicates from being displayed
 *   +added several divs to make ToC themable via CSS
 *   +provided basic CSS
 */
define('TABLEOFCONTENTS_REMOVE_PATTERN', '/(?:<p(?:\\s[^>]*)?' . '>)?\\[toc(?:\\s[^]]*?)?\\](?:<\\/p\\s*>)?/');
define('TABLEOFCONTENTS_ALLOWED_TAGS', '<em> <i> <strong> <b> <u> <del> <ins> <sub> <sup> <cite> <strike> <s> <tt> <span> <font> <abbr> <acronym> <dfn> <q> <bdo> <big> <small>');

/**
 * Implementation of hook_init()
 *
 * We load the JS/CSS files here so we can cache the filter results.
 */
function tableofcontents_init() {

  // we probably should not do that on non-node pages!
  // (how do you determine that?)
  // TODO: This can be possibly moved to hook_node_view. Revisit to check if
  // any other JS setting need to be added.
  $path = drupal_get_path('module', 'tableofcontents');
  drupal_add_js($path . '/jquery.scrollTo-min.js');
  drupal_add_js($path . '/jquery.localscroll-min.js');
  drupal_add_js($path . '/tableofcontents.js');
  drupal_add_css($path . '/tableofcontents.css');

  // Add any JS settings needed too.
  $settings = array(
    'tableOfContents' => array(
      'boxSettings' => array(
        'hideText' => variable_get('tableofcontents_hide_show_text_hide', 'hide'),
        'showText' => variable_get('tableofcontents_hide_show_text_show', 'show'),
      ),
    ),
  );
  drupal_add_js($settings, 'setting');
}

/**
 * Implementation of hook_help().
 */
function tableofcontents_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('A module to create a table of contents based on HTML header tags. Changes headers to anchors for processing so it may be incompatible with other filters that process header tags. It does use existing IDs on the header tags if already present and only operates on header levels 1 - 6.');
  }
}

/**
 * Implementation of hook_filter_FILTER_tips().
 */
function tableofcontents_filter_FILTER_tips($filter, $format, $long = FALSE) {
  if ($filter->name == filter_toc) {
    $delta = 0;
  }
  else {
    $delta = 1;
  }
  $var = $format->format;
  $override = variable_get('tableofcontents_allow_override_' . $var, $default = TRUE);
  $op = $delta * 4 | ($long ? 2 : 0) | ($override ? 1 : 0);
  switch ($op) {

    // ($delta, $long, $override)
    case 0:

      // (0, 0, 0)
      return t('Use [toc ...] to insert a mediawiki style collapsible table of contents.');
    case 1:

      // (0, 0, 1)
      return t('Use [toc list: ol; title: Table of Contents; minlevel: 2; maxlevel: 3; attachments: yes;] to insert a mediawiki style collapsible table of contents. All the arguments are optional.');
    case 2:

      // (0, 1, 0)
      return t('Every instance of "[toc ...]" in the input text will be replaced with a collapsible mediawiki-style table of contents.');
    case 3:

      // (0, 1, 1)
      return t('Every instance of "[toc ...]" in the input text will be replaced with a collapsible mediawiki-style table of contents. Accepts options for title, list style, minimum heading level, and maximum heading level, and attachments as follows: [toc list: ol; title: Table of Contents; minlevel: 2; maxlevel: 3; attachments: yes;]. All arguments are optional.');
    case 4:

    // (1, 0, 0)
    case 5:

      // (1, 0, 1)
      return t('Add an identifier to all the anchors without one.');
    case 6:

    // (1, 1, 0)
    case 7:

      // (1, 1, 1)
      return t('Add an identifier to all the anchors without one. In most cases, this filter is not necessary with the table of contents block. However, once in a while, the automatic detection may fail. In those cases, add this filter to your pages so as to make sure to get all the anchors defined as necessary for the table of contents links to work.');
  }
}

/**
 * Implementation of hook_filter_info().
 */
function tableofcontents_filter_info() {
  module_load_include('admin.inc', 'tableofcontents');
  $filters['filter_toc'] = array(
    'title' => t('Table of contents'),
    'description' => t('Inserts a table of contents in place of [toc ...] tags.'),
    'process callback' => '_tableofcontents_process',
    'prepare callback' => '_tableofcontents_prepare',
    'settings callback' => '_tableofcontents_settings',
    'cache' => TRUE,
    'tips callback' => 'tableofcontents_filter_tips',
  );
  $filters['filter_toc_id'] = array(
    'title' => t('Assign an ID to each anchors'),
    'description' => t('Add an ID to all the anchors on the page. May be necessary in case the table of contents block is used.'),
    'process callback' => '_tableofcontents_process',
    'prepare callback' => '_tableofcontents_prepare',
    'settings callback' => '_tableofcontents_settings',
    'cache' => TRUE,
    'tips callback' => 'tableofcontents_filter_tips',
  );
  return $filters;
}

/**
 * Save the extra data the TOC adds to nodes.
 */
function _tableofcontents_save($node) {

  // new nodes will not have our parameter set, make sure we have a default
  if (!isset($node->tableofcontents_toc_automatic)) {
    $node->tableofcontents_toc_automatic = 0;
  }
  $num_updated = db_update('tableofcontents_node_toc')
    ->fields(array(
    'toc_automatic' => $node->tableofcontents_toc_automatic,
  ))
    ->condition('nid', $node->nid)
    ->execute();
  if ($num_updated == 0) {
    db_insert('tableofcontents_node_toc')
      ->fields(array(
      'nid' => $node->nid,
      'toc_automatic' => $node->tableofcontents_toc_automatic,
    ))
      ->execute();
  }
}

/**
 * Load the extra data for the node from the TOC table.
 */
function _tableofcontents_load(&$node) {
  $result = db_select('tableofcontents_node_toc', 'toc')
    ->fields('toc', array(
    'toc_automatic',
  ))
    ->condition('nid', $node->nid)
    ->execute()
    ->fetch();
  if (!empty($result) && ($toc = $result->toc_automatic)) {
    $node->tableofcontents_toc_automatic = $toc;
  }
  else {
    $node->tableofcontents_toc_automatic = 0;
  }
}

/**
 * Implementation of hook_node_prepare
 */
function tableofcontents_node_prepare($node) {
  if (isset($node->files) && isset($node->body)) {

    // Remove the cached version if there are attachments on this node
    $cid = $node->format . ':' . md5($node->body);
    cache_clear_all($cid, 'cache_filter');
  }
}

/**
 * Implementation of hook_node_presave
 */
function tableofcontents_node_presave($node) {
  if (isset($node->body)) {
    if (variable_get('tableofcontents_remove_teaser_' . $node->body[$node->language][0]['format'], TRUE)) {
      module_load_include('admin.inc', 'tableofcontents');
      _tableofcontents_hide_in_teaser($node);
    }
  }
}

/**
 * Implementation of hook_node_insert
 */
function tableofcontents_node_insert($node) {
  _tableofcontents_save($node);
}

/**
 * Implementation of hook_node_update
 */
function tableofcontents_node_update($node) {
  _tableofcontents_save($node);
}

/**
 * Implementation of hook_node_load
 */
function tableofcontents_node_load($nodes) {
  foreach ($nodes as $node) {
    _tableofcontents_load($node);
  }
}

/**
 * Implementation of hook_node_view
 */
function tableofcontents_node_view($node, $view_mode) {
  global $user, $theme_key, $_tableofcontents_block_processing;
  if (!$_tableofcontents_block_processing && isset($node->content['body'])) {
    $processed = FALSE;
    if (variable_get('tableofcontents_nodetype_toc_vtoc_' . $node->type, FALSE)) {

      // ugly test to make sure we don't double the TOC (i.e. if automatic is turned
      // on we would apply the TOC twice when [toc] was used and the filter includes
      // the TOC!)
      if (strpos($node->content['body'][0]['#markup'], 'class="toc"') === FALSE) {
        $node->content['body'][0]['#markup'] = str_replace('[vtoc', '[toc', $node->content['body'][0]['#markup']);
        if ($view_mode == 'teaser' && variable_get('tableofcontents_nodetype_toc_remove_from_teaser_' . $node->type, TRUE)) {

          // remove from teaser or "non-page"
          $node->content['body'][0]['#markup'] = preg_replace(TABLEOFCONTENTS_REMOVE_PATTERN, '', $node->content['body'][0]['#markup']);
        }
        else {

          // TODO: if $a3 is true, then we should process the body and save the resulting table in the teaser
          //       (in other words, make sure we get the complete table of content instead of only the teaser part!)
          module_load_include('pages.inc', 'tableofcontents');

          //$format = new stdClass();
          $format = $node->content['body']['#items'][0]['format'];
          $text = _tableofcontents_process($node->content['body'][0]['#markup'], $format);
          if ($node->content['body'][0]['#markup'] != $text) {

            // if tableofcontents_hide_table_<format> is TRUE, then this is wrong...
            if (!variable_get('tableofcontents_hide_table_' . $node->content['body']['#items'][0]['format'], FALSE)) {
              $processed = TRUE;
            }
            $node->content['body'][0]['#markup'] = $text;
          }
        }
      }
    }
    if (!$processed) {

      // is the table of contents block visible?
      $rids = array_keys($user->roles);
      $query = db_select('block', 'b')
        ->fields('b', array(
        'bid',
      ));
      $query
        ->leftJoin('block_role', 'r', 'b.module = r.module AND b.delta = r.delta', array());
      $query
        ->condition('b.delta', 'tableofcontents_block')
        ->condition('b.module', 'tableofcontents_block')
        ->condition('b.theme', array_merge(array(
        $theme_key,
      ), $rids))
        ->condition('b.status', '1')
        ->condition(db_or()
        ->condition('r.rid', $rids, 'IN')
        ->isNull('r.rid'));
      $result = $query
        ->execute()
        ->fetchAll();
      if ($result) {

        // there is a table of contents block, but the node was node parsed...
        // do that now
        module_load_include('pages.inc', 'tableofcontents');
        $format = $node->content['body']['#items'][0]['format'];
        $node->content['body'][0]['#markup'] = _tableofcontents_process($node->content['body'][0]['#markup'], $format, 1);
      }
    }
  }
}

/**
 * Add a field in nodes so one can mark the node as using a TOC without
 * the need for the [toc] tag.
 */
function tableofcontents_form_alter(&$form, $form_state, $form_id) {
  if (!$form_state['submitted']) {
    if (substr($form_id, -10) == '_node_form' && isset($form['nid'])) {
      module_load_include('admin.inc', 'tableofcontents');
      _tableofcontents_node_form_alter($form);
    }
    elseif ($form_id == 'node_type_form') {
      module_load_include('admin.inc', 'tableofcontents');
      _tableofcontents_nodetype_form_alter($form);
    }
    elseif ($form_id == 'filter_admin_format_form') {
      $form['#submit'][] = '_tableofcontents_settings_submit';
    }
  }
}

/**
 * Implementation of hook_theme().
 *
 * @return
 *   Array of theme hooks this module implements.
 */
function tableofcontents_theme() {
  return array(
    'tableofcontents_toc' => array(
      'variables' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
    'tableofcontents_toc_text' => array(
      'arguments' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
    'tableofcontents_back_to_top' => array(
      'variables' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
    'tableofcontents_number' => array(
      'variables' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
    'tableofcontents_number_text' => array(
      'arguments' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
  );
}

// vim: ts=2 sw=2 et syntax=php

Functions

Namesort descending Description
tableofcontents_filter_FILTER_tips Implementation of hook_filter_FILTER_tips().
tableofcontents_filter_info Implementation of hook_filter_info().
tableofcontents_form_alter Add a field in nodes so one can mark the node as using a TOC without the need for the [toc] tag.
tableofcontents_help Implementation of hook_help().
tableofcontents_init Implementation of hook_init()
tableofcontents_node_insert Implementation of hook_node_insert
tableofcontents_node_load Implementation of hook_node_load
tableofcontents_node_prepare Implementation of hook_node_prepare
tableofcontents_node_presave Implementation of hook_node_presave
tableofcontents_node_update Implementation of hook_node_update
tableofcontents_node_view Implementation of hook_node_view
tableofcontents_theme Implementation of hook_theme().
_tableofcontents_load Load the extra data for the node from the TOC table.
_tableofcontents_save Save the extra data the TOC adds to nodes.

Constants

Namesort descending Description
TABLEOFCONTENTS_ALLOWED_TAGS
TABLEOFCONTENTS_REMOVE_PATTERN @file This is a filter module to generate a collapsible jquery enabled mediawiki style table of contents based on <h[1-6]> tags. Transforms header tags into named anchors.