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 '' . $fn['value'] . ''; } /** * 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 = '
    '; // 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 .= '
  • ' . $fn['value'] . '. '; $str .= $fn['text'] . "
  • \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 .= '
  • ' . $fn['value'] . '. '; foreach ($fn['ref_id'] as $ref) { $str .= '' . $abc[$i] . '. '; $i++; } $str .= $fn['text'] . "
  • \n"; } } $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']; }