123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- <?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 http://drupal.org/node/87226
- $footer = '';
- $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. (fixes 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'];
- }
|