You are here

footnotes.module in Footnotes 5

The Footnotes module is a filter that can be used to insert automatically numbered footnotes into Drupal texts.

Currently there are two filters. One is suitable for use primarily with html markup, but can be used with any input format. The second filter outputs footnotes in Textile format. This means you should run this filter together and before the Textile filter.

File

footnotes.module
View source
<?php

/**
 * @file
 * The Footnotes module is a filter that can be used to insert
 * automatically numbered footnotes into Drupal texts.
 *
 * Currently there are two filters. One is suitable for use primarily with html markup,
 * but can be used with any input format. The second filter outputs footnotes in 
 * Textile format. This means you should run this filter together and before the 
 * Textile filter.
 */

/**
 * Implementation of hook_help().
 *
 * Throughout Drupal, hook_help() is used to display help text at the top of
 * pages. Some other parts of drupal pages get explanatory text from these hooks
 * as well. We use it here to provide a description of the module on the
 * module administration page.
 */
function footnotes_help($section) {
  switch ($section) {
    case 'admin/modules#description':

      // This description is shown in the listing at admin/modules.
      return t('A filter to insert automatically numbered footnotes into Drupal texts.');
  }
}

/**
 * Implementation of hook_filter_tips().
 *
 * This hook allows filters to provide help text to users during the content
 * editing process. Short tips are provided on the content editing screen, while
 * long tips are provided on a separate linked page. Short tips are optional,
 * but long tips are highly recommended.
 */
function footnotes_filter_tips($delta, $format, $long = FALSE) {
  switch ($delta) {
    case 0:
      if ($long) {
        return t('You can insert footnotes directly into texts with <code>&lt;fn&gt;This text becomes a footnote.&lt;/fn&gt;</code>. This will be replaced with a running number (the footnote reference) and the text within the &lt;fn&gt; tags will be moved to the bottom of the page (the footnote).');
      }
      else {
        return t('Use &lt;fn&gt;...&lt;/fn&gt; to insert automatically numbered footnotes.');
      }
      break;
    case 1:
      if ($long) {
        return t('You can insert footnotes directly into texts with [# ...]. This will be replaced with a running number (the footnote reference) and the text within the [# ...] tags will be moved to the bottom of the page (the footnote). <em>This filter outputs footnotes in Textile format. You should use it together and before the Textile filter.</em>');
      }
      else {
        return t('Use [# ...] to insert automatically numbered footnotes. Textile variant.');
      }
      break;
  }
}

/**
 * Implementation of hook_filter().
 *
 * The bulk of filtering work is done here. This hook is quite complicated, so
 * we'll discuss each operation it defines.
 */
function footnotes_filter($op, $delta = 0, $format = -1, $text = '') {

  // The "list" operation provides the module an opportunity to declare both how
  // many filters it defines and a human-readable name for each filter. Note that
  // the returned name should be passed through t() for translation.
  if ($op == 'list') {
    return array(
      0 => t('Footnotes &lt;fn&gt;'),
      1 => t('Footnotes Textile style'),
    );
  }

  // All operations besides "list" provide a $delta argument so we know which
  // filter they refer to. We'll switch on that argument now so that we can
  // discuss each filter in turn.
  switch ($delta) {

    // First is the html footnotes filter
    case 0:
      switch ($op) {

        // This description is shown in the administrative interface, unlike the
        // filter tips which are shown in the content editing interface.
        case 'description':
          return t('Use &lt;fn&gt;...&lt;/fn&gt; to insert automatically numbered footnotes.');

        // We don't need the "prepare" operation for this filter, but it's required
        // to at least return the input text as-is.

        //TODO: May need to escape <fn> if we use HTML filter too, but Footnotes could be first
        case 'prepare':
          return $text;

        // The actual filtering is performed here. The supplied text should be
        // returned, once any necessary substitutions have taken place.
        case 'process':
          $text = preg_replace_callback('|<fn>(.*?)</fn>|s', '_footnotes_replace_callback', $text);

          //Replace tag <footnotes> with the list of footnotes.

          //If tag is not present, by default add the footnotes at the end.

          //Thanks to acp on drupal.org for this idea. see http://drupal.org/node/87226
          $footer = '';
          $footer = _footnotes_replace_callback(NULL, 'output footer');
          if (preg_match('/<footnotes(\\/( )?)?>/', $text) > 0) {
            $text = preg_replace('/<footnotes(\\/( )?)?>/', $footer, $text, 1);
            return $text;
          }
          else {
            return $text . "\n\n" . $footer;
          }
      }
      break;

    // Textile version.
    case 1:
      switch ($op) {

        // This description is shown in the administrative interface, unlike the
        // filter tips which are shown in the content editing interface.
        case 'description':
          return t('Use [# ...] to insert automatically numbered footnotes in Textile markup.');

        // We don't need the "prepare" operation for this filter, but it's required
        // to at least return the input text as-is.
        case 'prepare':
          return $text;

        // The actual filtering is performed here. The supplied text should be
        // returned, once any necessary substitutions have taken place.
        case 'process':
          $text = preg_replace_callback('|\\[# (.*?)\\]|s', '_footnotes_replace_callback_textile', $text);

          //Replace Textile tag "footnotes." with the list of footnotes.

          //If tag is not present, by default add the footnotes at the end.

          //Thanks to acp on drupal.org for this idea. see http://drupal.org/node/87226
          $footer = '';
          $footer = _footnotes_replace_callback_textile(NULL, 'output footer');
          if (preg_match('/\\n *footnotes\\. *(\\n|$)/', $text) > 0) {
            $text = preg_replace('/\\n *footnotes\\. *(\\n|$)/', "\n{$footer}\n", $text, 1);
            return $text;
          }
          else {
            return $text . "\n\n" . $footer;
          }
      }
      break;
  }
}

/**
 * Helper function called from preg_replace_callback() above
 *
 * Uses static vars to temporarily store footnotes found.
 * In my understanding, this is not threadsafe?!
 */
function _footnotes_replace_callback($matches, $op = '') {
  static $n = 0;
  static $store_matches = array();
  $str = '';
  if ($op == 'output footer') {
    if ($n > 0) {
      $str = '<ol class="footnotes">';
      for ($m = 1; $m <= $n; $m++) {
        $text = $store_matches[$m - 1][0];
        $randstr = $store_matches[$m - 1][1];
        $str .= '<li><a class="footnote" name="footnote' . $m . '_' . $randstr . '" href="#footnoteref' . $m . '_' . $randstr . '">' . $m . '.</a> ';
        $str .= $text . "</li>\n";
      }
      $str .= "</ol>\n";
    }
    $n = 0;
    $store_matches = array();
    return $str;
  }

  //default op: act as called by preg_replace_callback()

  //Random string used to ensure footnote id's are unique, even

  //when contents of multiple nodes reside on same page. (fixes http://drupal.org/node/194558)
  $randstr = _footnotes_helper_randstr();
  array_push($store_matches, array(
    $matches[1],
    $randstr,
  ));
  $n++;
  $allowed_tags = array();
  $title = filter_xss($matches[1], $allowed_tags);

  //html attribute cannot contain quotes
  $title = str_replace('"', "&quot;", $title);

  //remove newlines. Browsers don't support them anyway and they'll confuse line break converter in filter.module
  $title = str_replace("\n", " ", $title);
  $title = str_replace("\r", "", $title);
  return '<a class="see_footnote" id="footnoteref' . $n . '_' . $randstr . '" title="' . $title . '" href="#footnote' . $n . '_' . $randstr . '">' . $n . '</a>';
}

/**
 * Helper function called from preg_replace_callback() above
 *
 * Uses static vars to temporarily store footnotes found.
 * In my understanding, this is not threadsafe?!
 */
function _footnotes_replace_callback_textile($matches, $op = '') {
  static $n = 0;
  static $store_matches = array();
  $str = '';
  if ($op == 'output footer') {
    if ($n > 0) {
      $str = '';
      for ($m = 1; $m <= $n; $m++) {
        $str .= "fn{$m}. " . $store_matches[$m - 1] . "\n\n";
      }
    }
    $n = 0;
    $store_matches = array();
    return $str;
  }

  //default op: act as called by preg_replace_callback()
  array_push($store_matches, $matches[1]);
  $n++;
  return '[' . $n . ']';
}

/**
 * Helper function to return a random text string
 *
 * @return random (lowercase) alphanumeric string
 */
function _footnotes_helper_randstr() {
  $chars = "abcdefghijklmnopqrstuwxyz1234567890";
  $str = "";

  //seeding with srand() not neccessary in modern PHP versions
  for ($i = 0; $i < 7; $i++) {
    $n = rand(0, strlen($chars) - 1);
    $str .= substr($chars, $n, 1);
  }
  return $str;
}

/**
* Implementation of hook_menu()
*
* Add special css for Footnotes module.
*
* Thanks to binford2k@lug.wsu.edu for this tip and drinkypoo
* for the question leading up to it. http://drupal.org/node/80538
*/
function footnotes_menu($may_cache) {
  if (!$may_cache) {
    drupal_add_css(drupal_get_path('module', 'footnotes') . '/footnotes.css');
  }
}

/**
* Helper for other filters, check if Footnotes is present in your filter chain.
*
* Other filters may leverage the Footnotes functionality in a simple way:
* by outputting markup with <fn>...</fn> tags within. 

* This creates a dependency, the Footnotes filter must be present later in 
* "Input format". By calling this helper function the other filters that 
* depend on Footnotes may check whether Footnotes is present later in the chain
* of filters in the current Input format.
*
* If this function returns true, the caller may depend on Footnotes. Function returns
* false if caller may not depend on Footnotes.
* 
* Example usage:
* <code>
* filter_example_filter( $op, $delta = 0, $format = -1, $text = '') {
*   ...
*   //When caller wishes to depend on html footnotes, last argument may be omitted
*   if( footnotes_is_footnotes_later( $format, 'filter_example_filter', $delta ) ) {
*     //output markup which may include <fn> tags
*   }
*   else {
*     // must make do without footnotes features
*   }
*   ...
* }
* </code>
*
* Note: You should also put "dependencies = footnotes" in your module.info file.
*
* @param $format
*    The input format caller is being run as part of ($format of hook_filter(...))
* @param $caller
*    Name of calling module
* @param $caller_delta
*    Delta of the filter within calling module ($delta of hook_filter(...))
* @param $footnotes_delta
*    Delta of the filter within footnotes module
*
* @return True if Footnotes is present after $caller in Input format $format
*/
function footnotes_is_footnotes_later($format, $caller, $caller_delta = 0, $footnotes_delta = 0) {

  //Determine caller's weight in the current input format
  $result = db_query("SELECT weight FROM filters WHERE module='%s' AND format=%d AND delta=%d;", $caller, $format, $caller_delta);
  $caller_weight = db_fetch_object($result);
  $caller_weight = $caller_weight->weight;

  //See if Footnotes is present in the input format and if weight is higher
  $result = db_query("SELECT weight FROM filters WHERE module='%s' AND format=%d AND delta=%d;", 'footnotes', $format, $footnotes_delta);
  $fn_weight = db_fetch_object($result);
  if ($fn_weight) {
    $fn_weight = $fn_weight->weight;
  }
  else {

    //Footnotes is not present at all in input format $format
    return FALSE;
  }
  if ($fn_weight > $caller_weight) {

    //Footnotes is after caller in input format $format
    return TRUE;
  }
  else {

    //Footnotes is before caller in input format $format
    return FALSE;

    //TODO: What is correct interpretation when weight is equal?
  }
}

Functions

Namesort descending Description
footnotes_filter Implementation of hook_filter().
footnotes_filter_tips Implementation of hook_filter_tips().
footnotes_help Implementation of hook_help().
footnotes_is_footnotes_later Helper for other filters, check if Footnotes is present in your filter chain.
footnotes_menu Implementation of hook_menu()
_footnotes_helper_randstr Helper function to return a random text string
_footnotes_replace_callback Helper function called from preg_replace_callback() above
_footnotes_replace_callback_textile Helper function called from preg_replace_callback() above