|
|
|
@ -2,8 +2,9 @@
|
|
|
|
|
|
|
|
|
|
namespace Drupal\bibcite_footnotes\Plugin\Filter; |
|
|
|
|
|
|
|
|
|
use Drupal\footnotes\Plugin\Filter\FootnotesFilter; |
|
|
|
|
use Drupal\Component\Utility\Xss; |
|
|
|
|
use Drupal\Core\Form\FormStateInterface; |
|
|
|
|
use Drupal\footnotes\Plugin\Filter\FootnotesFilter; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Provides a base filter for Reference Footnotes filter. |
|
|
|
@ -98,4 +99,207 @@ class ReferenceFootnotesFilter extends FootnotesFilter {
|
|
|
|
|
return $settings; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Helper function called from preg_replace_callback() above. |
|
|
|
|
* |
|
|
|
|
* Uses static vars to temporarily store footnotes found. |
|
|
|
|
* This is not threadsafe, but PHP isn't. |
|
|
|
|
* |
|
|
|
|
* @param array $matches |
|
|
|
|
* Elements from array: |
|
|
|
|
* - 0: complete matched string. |
|
|
|
|
* - 1: tag name. |
|
|
|
|
* - 2: tag attributes. |
|
|
|
|
* - 3: tag content. |
|
|
|
|
* @param string $op |
|
|
|
|
* Operation. |
|
|
|
|
* |
|
|
|
|
* @return string |
|
|
|
|
* Return the string processed by geshi library. |
|
|
|
|
*/ |
|
|
|
|
protected function replaceCallback($matches, $op = '') { |
|
|
|
|
static $opt_collapse = 0; |
|
|
|
|
static $n = 0; |
|
|
|
|
static $store_matches = []; |
|
|
|
|
static $used_values = []; |
|
|
|
|
$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. |
|
|
|
|
// @FIXME |
|
|
|
|
// theme() has been renamed to _theme() and should NEVER be called |
|
|
|
|
// directly. Calling _theme() directly can alter the expected output and |
|
|
|
|
// potentially introduce security issues |
|
|
|
|
// (see https://www.drupal.org/node/2195739). You should use renderable |
|
|
|
|
// arrays instead. @see https://www.drupal.org/node/2195739 |
|
|
|
|
$markup = [ |
|
|
|
|
'#theme' => 'footnote_list', |
|
|
|
|
'#footnotes' => $store_matches, |
|
|
|
|
]; |
|
|
|
|
$str = \Drupal::service('renderer')->render($markup, FALSE); |
|
|
|
|
} |
|
|
|
|
// Reset the static variables so they can be used again next time. |
|
|
|
|
$n = 0; |
|
|
|
|
$store_matches = []; |
|
|
|
|
$used_values = []; |
|
|
|
|
|
|
|
|
|
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 = $this->randstr(); |
|
|
|
|
|
|
|
|
|
$value = $this->extractAtribute($matches, 'value'); |
|
|
|
|
|
|
|
|
|
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 = $this->findFootnote($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); |
|
|
|
|
|
|
|
|
|
$page = $this->extractAtribute($matches, 'page'); |
|
|
|
|
|
|
|
|
|
// 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 = []; |
|
|
|
|
$text_clean = Xss::filter($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 = [ |
|
|
|
|
'value' => $value, |
|
|
|
|
'text' => $matches[2], |
|
|
|
|
'text_clean' => $text_clean, |
|
|
|
|
'page' => $page, |
|
|
|
|
'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 occurrence 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'] : [$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 = [ |
|
|
|
|
'#theme' => 'footnote_link', |
|
|
|
|
'fn' => $fn, |
|
|
|
|
]; |
|
|
|
|
$result = \Drupal::service('renderer')->render($fn, FALSE); |
|
|
|
|
|
|
|
|
|
return $result; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Search the $store_matches array for footnote text that matches. |
|
|
|
|
* |
|
|
|
|
* 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. |
|
|
|
|
* |
|
|
|
|
* @param string $text |
|
|
|
|
* The footnote text. |
|
|
|
|
* @param array $store_matches |
|
|
|
|
* The matches array. |
|
|
|
|
* |
|
|
|
|
* @return string|false |
|
|
|
|
* The value of the existing footnote, FALSE otherwise. |
|
|
|
|
*/ |
|
|
|
|
private function findFootnote($text, &$store_matches) { |
|
|
|
|
if (!empty($store_matches)) { |
|
|
|
|
foreach ($store_matches as &$fn) { |
|
|
|
|
if ($fn['text'] == $text) { |
|
|
|
|
return $fn['value']; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return FALSE; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @param $matches |
|
|
|
|
* @param $value_match |
|
|
|
|
* |
|
|
|
|
* @return string |
|
|
|
|
*/ |
|
|
|
|
protected function extractAtribute($matches, $attribute): string { |
|
|
|
|
$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('|' . $attribute . '=["\'](.*?)["\']|', $matches[1], $value_match)) { |
|
|
|
|
$value = $value_match[1]; |
|
|
|
|
// Or without quotes eg <fn value=8>. |
|
|
|
|
} |
|
|
|
|
elseif (preg_match('|' . $attribute . '=(\S*)|', $matches[1], $value_match)) { |
|
|
|
|
$value = $value_match[1]; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return $value; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|