here.', 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 [fn]This text becomes a footnote.[/fn]
. 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'=>'' . t('Footnotes Readme page') . ''));
}
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 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([^\]]*)\]|', '', $text);
$text = preg_replace( '|\[/fn\]|', '', $text);
$text = preg_replace( '|\[footnotes([^\]]*)\]|', '', $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("|]*)>|", $text, $foo);
$close_tags = preg_match_all("||", $text, $foo);
if ($open_tags == $close_tags + 1) {
$text = $text . '';
}
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 = '|]*)>(.*?)|s';
$text = preg_replace_callback($pattern , '_footnotes_replace_callback', $text);
// Replace tag 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');
$pattern = '|()|';
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. (fixes http://drupal.org/node/194558)
$randstr = _footnotes_helper_randstr();
$value = '';
// Did the pattern match anything in the tag?
if ($matches[1]) {
// See if value attribute can parsed, either well-formed in quotes eg
if (preg_match('|value=["\'](.*?)["\']|',$matches[1],$value_match)) {
$value = $value_match[1];
// Or without quotes eg
} 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 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 '';
}
/**
* 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 = '\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 ... 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:
*
* _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
* }
* ...
* }
*
*
* @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'];
}