footnotes.module in Footnotes 7.3
Same filename and directory in other branches
The Footnotes module is a filter that can be used to insert automatically numbered footnotes into Drupal texts.
File
footnotes.moduleView source
<?php
/**
* @file
* The Footnotes module is a filter that can be used to insert
* automatically numbered footnotes into Drupal texts.
*/
/**
* Implementation of hook_help().
*/
function footnotes_help($path, $arg) {
switch ($path) {
case 'admin/help#footnotes':
// This description is shown in the listing at admin/modules.
return t('Insert automatically numbered footnotes using <fn> or [fn] tags. Enable the footnotes text filter <a href="@url">here</a>.', array(
'@url' => url('admin/config/content/formats'),
));
}
}
/**
* Implementation of hook_filter_info().
*/
function footnotes_filter_info() {
$filters['filter_footnotes'] = array(
'title' => t('Footnotes'),
'description' => t('Insert automatically numbered footnotes using <fn> or [fn] tags.'),
'process callback' => '_footnotes_filter',
'settings callback' => '_footnotes_settings',
'default settings' => array(
'footnotes_collapse' => 0,
),
'tips callback' => '_footnotes_filter_tips',
'weight' => -20,
);
return $filters;
}
/**
* 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($filter, $format, $long = FALSE) {
if ($long) {
return t('You can insert footnotes directly into texts with <code>[fn]This text becomes a footnote.[/fn]</code>. This will be replaced with a running number (the footnote reference) and the text within the [fn] tags will be moved to the bottom of the page (the footnote). See %link for additional usage options.', array(
'%link' => '<a href="http://drupal.org/project/footnotes">' . t('Footnotes Readme page') . '</a>',
));
}
else {
return t('Use [fn]...[/fn] (or <fn>...</fn>) to insert automatically numbered footnotes.');
}
}
/**
* Options for the Footnotes filter.
*
* This has currently 1 setting, the feature to collapse together footnotes
* with identical content is an option.
*/
function _footnotes_settings($form, &$form_state, $filter, $format, $defaults, $filters) {
$settings['footnotes_collapse'] = array(
'#type' => 'checkbox',
'#title' => t('Collapse footnotes with identical content'),
'#default_value' => isset($filter->settings['footnotes_collapse']) ? $filter->settings['footnotes_collapse'] : $defaults['footnotes_collapse'],
'#description' => t('If two footnotes have the exact same content, they will be collapsed into one as if using the same value="" attribute.'),
);
return $settings;
}
/**
* The bulk of filtering work is done here.
*/
function _footnotes_filter($text = '', $filter, $format) {
// Supporting both [fn] and <fn> now.
// Thanks to fletchgqc http://drupal.org/node/268026.
// Convert all square brackets to angle brackets. This way all further code
// just manipulates angle brackets. (Angle brackets are preferred here for
// the simple reason that square brackets are more tedious to use in regexps.)
$text = preg_replace('|\\[fn([^\\]]*)\\]|', '<fn$1>', $text);
$text = preg_replace('|\\[/fn\\]|', '</fn>', $text);
$text = preg_replace('|\\[footnotes([^\\]]*)\\]|', '<footnotes$1>', $text);
// Check that there are an even number of open and closing tags.
// If there is one closing tag missing, append this to the end.
// If there is more disparity, throw a warning and continue.
// A closing tag may sometimes be missing when we are processing a teaser
// and it has been cut in the middle of the footnote.
// See http://drupal.org/node/253326
$foo = array();
$open_tags = preg_match_all("|<fn([^>]*)>|", $text, $foo);
$close_tags = preg_match_all("|</fn>|", $text, $foo);
if ($open_tags == $close_tags + 1) {
$text = $text . '</fn>';
}
elseif ($open_tags > $close_tags + 1) {
trigger_error(t("You have unclosed fn tags. This is invalid and will produce unpredictable results."));
}
// Before doing the replacement, the callback function needs to know which options to use.
_footnotes_replace_callback($filter->settings['footnotes_collapse'], 'prepare');
$pattern = '|<fn([^>]*)>(.*?)</fn>|s';
$text = preg_replace_callback($pattern, '_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 drupal.org/node/87226.
$footer = _footnotes_replace_callback(NULL, 'output footer');
$pattern = '|(<footnotes([^\\]]*)>)|';
if (preg_match($pattern, $text) > 0) {
$text = preg_replace($pattern, $footer, $text, 1);
return $text;
}
else {
return $text . "\n\n" . $footer;
}
}
/**
* Search the $store_matches array for footnote text that matches and return
* the value.
*
* Note: This does a linear search on the $store_matches array. For a large
* list of footnotes it would be more efficient to maintain a separate array
* with the footnote content as key, in order to do a hash lookup at this
* stage. Since you typically only have a handful of footnotes, this simple
* search is assumed to be more efficient.
* (but was not tested).
*
* @author djdevin (see http://drupal.org/node/808214)
*
* @param string The footnote text
* @param array The matches array
*
* @return mixed The value of the existing footnote, FALSE otherwise
*/
function _footnotes_helper_find_footnote($text, &$store_matches) {
if (!empty($store_matches)) {
foreach ($store_matches as &$fn) {
if ($fn['text'] == $text) {
return $fn['value'];
}
}
}
return FALSE;
}
/**
* Helper function called from preg_replace_callback() above
*
* Uses static vars to temporarily store footnotes found.
* This is not threadsafe, but PHP isn't.
*/
function _footnotes_replace_callback($matches, $op = '') {
static $opt_collapse = 0;
static $n = 0;
static $store_matches = array();
static $used_values = array();
$str = '';
if ($op == 'prepare') {
// In the 'prepare' case, the first argument contains the options to use.
// The name 'matches' is incorrect, we just use the variable anyway.
$opt_collapse = $matches;
return 0;
}
if ($op == 'output footer') {
if (count($store_matches) > 0) {
// Only if there are stored fn matches, pass the array of fns to be themed
// as a list
// Drupal 7 requires we use "render element" which just introduces a wrapper
// around the old array.
$str = theme('footnote_list', array(
'footnotes' => $store_matches,
));
}
// Reset the static variables so they can be used again next time
$n = 0;
$store_matches = array();
$used_values = 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. Dixes http://drupal.org/node/194558.
$randstr = _footnotes_helper_randstr();
$value = '';
// Did the pattern match anything in the <fn> tag?
if ($matches[1]) {
// See if value attribute can parsed, either well-formed in quotes eg <fn value="3">
if (preg_match('|value=["\'](.*?)["\']|', $matches[1], $value_match)) {
$value = $value_match[1];
// Or without quotes eg <fn value=8>
}
elseif (preg_match('|value=(\\S*)|', $matches[1], $value_match)) {
$value = $value_match[1];
}
}
if ($value) {
// A value label was found. If it is numeric, record it in $n so further notes
// can increment from there.
// After adding support for multiple references to same footnote in the body (http://drupal.org/node/636808)
// also must check that $n is monotonously increasing
if (is_numeric($value) && $n < $value) {
$n = $value;
}
}
elseif ($opt_collapse and $value_existing = _footnotes_helper_find_footnote($matches[2], $store_matches)) {
// An identical footnote already exists. Set value to the previously existing value.
$value = $value_existing;
}
else {
// No value label, either a plain <fn> or unparsable attributes. Increment the
// footnote counter, set label equal to it.
$n++;
$value = $n;
}
// Remove illegal characters from $value so it can be used as an HTML
// id attribute.
$value_id = preg_replace('|[^\\w\\-]|', '', $value);
// Create a sanitized version of $text that is suitable for using as HTML
// attribute value. (In particular, as the title attribute to the footnote link.)
$allowed_tags = array();
$text_clean = filter_xss($matches['2'], $allowed_tags);
// HTML attribute cannot contain quotes
$text_clean = str_replace('"', """, $text_clean);
// Remove newlines. Browsers don't support them anyway and they'll confuse
// line break converter in filter.module.
$text_clean = str_replace("\n", " ", $text_clean);
$text_clean = str_replace("\r", "", $text_clean);
// Create a footnote item as an array.
$fn = array(
'value' => $value,
'text' => $matches[2],
'text_clean' => $text_clean,
'fn_id' => 'footnote' . $value_id . '_' . $randstr,
'ref_id' => 'footnoteref' . $value_id . '_' . $randstr,
);
// We now allow to repeat the footnote value label, in which case the link to
// the previously existing footnote is returned. Content of the current
// footnote is ignored.
// See http://drupal.org/node/636808
if (!in_array($value, $used_values)) {
// This is the normal case, add the footnote to $store_matches.
// Store the footnote item.
array_push($store_matches, $fn);
array_push($used_values, $value);
}
else {
// A footnote with the same label already exists.
// Use the text and id from the first footnote with this value.
// Any text in this footnote is discarded.
$i = array_search($value, $used_values);
$fn['text'] = $store_matches[$i]['text'];
$fn['text_clean'] = $store_matches[$i]['text_clean'];
$fn['fn_id'] = $store_matches[$i]['fn_id'];
// Push the new ref_id into the first occurence of this footnote label
// The stored footnote thus holds a list of ref_id's rather than just one id
$ref_array = is_array($store_matches[$i]['ref_id']) ? $store_matches[$i]['ref_id'] : array(
$store_matches[$i]['ref_id'],
);
array_push($ref_array, $fn['ref_id']);
$store_matches[$i]['ref_id'] = $ref_array;
}
// Return the item themed into a footnote link.
// Drupal 7 requires we use "render element" which just introduces a wrapper
// around the old array.
$fn = array(
'fn' => $fn,
);
return theme('footnote_link', $fn);
}
/**
* 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_theme()
*
* Thanks to emfabric for this implementation. http://drupal.org/node/221156
*/
function footnotes_theme() {
return array(
'footnote_link' => array(
'render element' => 'fn',
),
'footnote_list' => array(
'render element' => 'footnotes',
),
);
}
/**
* Themed output of a footnote link appearing in the text body
*
* Accepts a single associative array, containing values on the following keys:
* text - the raw unprocessed text extracted from within the [fn] tag
* text_clean - a sanitized version of the previous, may be used as HTML
* attribute value value - the raw unprocessed footnote number or other
* identifying label fn_id - the globally unique identifier for the in-body
* footnote link anchor, used to allow links from the list to the body ref_id -
* the globally unique identifier for the footnote's anchor in the footnote
* listing, used to allow links to the list from the body
*/
function theme_footnote_link($fn) {
// Drupal 7 requires we use "render element" which just introduces a wrapper
// around the old array.
$fn = $fn['fn'];
return '<a class="see-footnote" id="' . $fn['ref_id'] . '" title="' . $fn['text_clean'] . '" href="#' . $fn['fn_id'] . '">' . $fn['value'] . '</a>';
}
/**
* Themed output of the footnotes list appearing at at [footnotes]
*
* Accepts an array containing an ordered listing of associative arrays, each
* containing values on the following keys:
* text - the raw unprocessed text extracted from within the [fn] tag
* text_clean - a sanitized version of the previous, may be used as HTML
* attribute value value - the raw unprocessed footnote number or other
* identifying label fn_id - the globally unique identifier for the in-body
* footnote link anchor, used to allow links from the list to the body ref_id -
* the globally unique identifier for the footnote's anchor in the footnote
* listing, used to allow links to the list from the body
*/
function theme_footnote_list($footnotes) {
$str = '<ul class="footnotes">';
// Drupal 7 requires we use "render element" which just introduces a wrapper
// around the old array.
$footnotes = $footnotes['footnotes'];
// loop through the footnotes
foreach ($footnotes as $fn) {
if (!is_array($fn['ref_id'])) {
// Output normal footnote.
$str .= '<li class="footnote" id="' . $fn['fn_id'] . '"><a class="footnote-label" href="#' . $fn['ref_id'] . '">' . $fn['value'] . '.</a> ';
$str .= $fn['text'] . "</li>\n";
}
else {
// Output footnote that has more than one reference to it in the body.
// The only difference is to insert backlinks to all references.
// Helper: we need to enumerate a, b, c...
$abc = str_split("abcdefghijklmnopqrstuvwxyz");
$i = 0;
$str .= '<li class="footnote" id="' . $fn['fn_id'] . '"><a href="#' . $fn['ref_id'][0] . '" class="footnote-label">' . $fn['value'] . '.</a> ';
foreach ($fn['ref_id'] as $ref) {
$str .= '<a class="footnote-multi" href="#' . $ref . '">' . $abc[$i] . '.</a> ';
$i++;
}
$str .= $fn['text'] . "</li>\n";
}
}
$str .= "</ul>\n";
return $str;
}
/**
* Helper for other filters, check if Footnotes is present in your filter
* chain.
*
* Note: Due to changes in Filter API, the arguments to this function have
* changed in Drupal 7.
*
* 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.
*
* You should also put "dependencies = footnotes" in your module.info file.
*
* Example usage:
* <code>
* _filter_example_process( $text, $filter, $format ) {
* ...
* if(footnotes_is_footnotes_later($format, $filter)) {
* //output markup which may include [fn] tags
* }
* else {
* // must make do without footnotes features
* // can also emit warning/error that user should install and configure
* footnotes module
* }
* ...
* }
* </code>
*
* @param $format
* The text format object caller is part of.
* @param $caller_filter
* The filter object representing the caller (in this text format).
*
* @return True if Footnotes is present after $caller in $format.
*/
function footnotes_is_footnotes_later($format, $caller_filter) {
return $format['filter_footnotes']['weight'] > $caller_filter['weight'];
}
Functions
Name![]() |
Description |
---|---|
footnotes_filter_info | Implementation of hook_filter_info(). |
footnotes_help | Implementation of hook_help(). |
footnotes_is_footnotes_later | Helper for other filters, check if Footnotes is present in your filter chain. |
footnotes_theme | Implementation of hook_theme() |
theme_footnote_link | Themed output of a footnote link appearing in the text body |
theme_footnote_list | Themed output of the footnotes list appearing at at [footnotes] |
_footnotes_filter | The bulk of filtering work is done here. |
_footnotes_filter_tips | 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. |
_footnotes_helper_find_footnote | Search the $store_matches array for footnote text that matches and return the value. |
_footnotes_helper_randstr | Helper function to return a random text string |
_footnotes_replace_callback | Helper function called from preg_replace_callback() above |
_footnotes_settings | Options for the Footnotes filter. |