diff --git a/bibcite_footnotes.module b/bibcite_footnotes.module
index 66b18b0..08d9987 100644
--- a/bibcite_footnotes.module
+++ b/bibcite_footnotes.module
@@ -71,12 +71,13 @@ function bibcite_footnotes_preprocess_footnote_list(&$variables) {
$build = [];
$reference_entity_id = _bibcite_footnotes_get_reference_entity_id_from_bibcite_footnote($fn['text']);
+ $footnote_link_text = $dont_show_backlink_text && $reference_entity_id ? '^' : $fn['value'];
if (!is_array($fn['ref_id'])) {
// Output normal footnote.
$url = Url::fromUserInput('#' . $fn['ref_id'], ['attributes' => ['id' => $fn['fn_id'], 'class' => 'footnote-link']]);
- $link = Link::fromTextAndUrl(($dont_show_backlink_text && $reference_entity_id ? '^' : $fn['value']), $url)->toRenderable();
+ $link = Link::fromTextAndUrl(($footnote_link_text), $url)->toRenderable();
$build[] = $link;
}
else {
@@ -87,7 +88,7 @@ function bibcite_footnotes_preprocess_footnote_list(&$variables) {
$i = 0;
$url = Url::fromUserInput('#' . $fn['ref_id'][0], ['attributes' => ['id' => $fn['fn_id'], 'class' => 'footnote-link']]);
- $build[] = Link::fromTextAndUrl($fn['value'], $url)->toRenderable();
+ $build[] = Link::fromTextAndUrl($footnote_link_text, $url)->toRenderable();
foreach ($fn['ref_id'] as $ref) {
$url = Url::fromUserInput( '#' . $ref, ['attributes' => ['id' => $fn['fn_id'], 'class' => 'footnote-multi']]);
@@ -144,27 +145,26 @@ function bibcite_footnotes_preprocess_footnote_link(&$variables) {
// $variables['fn']['fn']['#markup'] = '
Hello!
';
$fn = $variables['fn']['fn'];
// TODO: Make a more formal way to denote inline citations.
- $citation_is_inline = ($fn['value'][0] == '(' && $fn['value'][strlen($fn['value']) - 1] == ')') ? '-inline' : '';
- $class = 'see-footnote' . $citation_is_inline;
+
+ $class = 'see-footnote';
// Generate the hover text
$citation_tools = new CitationTools();
$citation_entity_id = _bibcite_footnotes_get_reference_entity_id_from_bibcite_footnote($fn['text']);
- $title = trim(strip_tags(render($citation_tools->getRenderableReference($citation_entity_id))));
+ $citation_data = $citation_tools->getRenderableReference($citation_entity_id);
+
+ // Citation contains a page reference, so construct parenthetical footnote.
+ if (!empty($fn['page'])) {
+ $fn['value'] = "({$fn['value']}, {$fn['page']})";
+ $citation_data['#data']['page'] = $fn['page'];
+ $class .= '-inline';
+ }
+
+ $title = trim(strip_tags(render($citation_data)));
$url = Url::fromUserInput('#' . $fn['fn_id'], ['attributes' => ['id' => $fn['ref_id'], 'class' => $class, 'title' => $title]]);
+
+
$link = Link::fromTextAndUrl($fn['value'], $url)->toRenderable();
$variables['fn']['fn'][] = $link;
-
-}
-
-/**
- * Implements hook_theme().
- */
-function bibcite_footnotes_theme() {
- return [
- 'bibcite_footnotes' => [
- 'render element' => 'children',
- ],
- ];
}
function bibcite_footnotes_theme_registry_alter(&$theme_registry) {
diff --git a/js/plugins/reference_footnotes/dialogs/footnotes.js b/js/plugins/reference_footnotes/dialogs/footnotes.js
index 5cac213..e616600 100644
--- a/js/plugins/reference_footnotes/dialogs/footnotes.js
+++ b/js/plugins/reference_footnotes/dialogs/footnotes.js
@@ -33,6 +33,18 @@
}
}
}
+ },
+ {
+ id: 'page',
+ type: 'text',
+ labelLayout: 'horizontal',
+ label: Drupal.t('Page(s):'),
+ style: 'float:left:width:50px',
+ setup: function (element) {
+ if (isEdit) {
+ this.setValue(element.getAttribute('page'));
+ }
+ }
},
{
id: 'value',
@@ -59,7 +71,8 @@
var referenceNote = this.getValueOf('info', 'reference_footnote');
var textNote = this.getValueOf('info', 'footnote');
var value = textNote ? textNote : referenceNote;
- CKEDITOR.plugins.reference_footnotes.createFootnote( editor, this.realObj, value, this.getValueOf('info', 'value'));
+ var page = this.getValueOf('info', 'page');
+ CKEDITOR.plugins.reference_footnotes.createFootnote( editor, this.realObj, value, this.getValueOf('info', 'value'), page);
delete this.fakeObj;
delete this.realObj;
}
diff --git a/js/plugins/reference_footnotes/plugin.js b/js/plugins/reference_footnotes/plugin.js
index 84fbe0d..c2392d7 100644
--- a/js/plugins/reference_footnotes/plugin.js
+++ b/js/plugins/reference_footnotes/plugin.js
@@ -29,10 +29,10 @@
init: function( editor )
{
editor.addCommand('createreferencefootnotes', new CKEDITOR.dialogCommand('createreferencefootnotes', {
- allowedContent: 'fn[value]'
+ allowedContent: 'fn[value][page]'
}));
editor.addCommand('editreferencefootnotes', new CKEDITOR.dialogCommand('editreferencefootnotes', {
- allowedContent: 'fn[value]'
+ allowedContent: 'fn[value][page]'
}));
// Drupal Wysiwyg requirement: The first argument to editor.ui.addButton()
@@ -94,7 +94,7 @@
})();
CKEDITOR.plugins.reference_footnotes = {
- createFootnote: function( editor, origElement, text, value) {
+ createFootnote: function( editor, origElement, text, value, page) {
if (!origElement) {
var realElement = CKEDITOR.dom.element.createFromHtml('');
}
@@ -106,6 +106,9 @@ CKEDITOR.plugins.reference_footnotes = {
realElement.setText(text);
}
realElement.setAttribute('value',value);
+ if (page && page.length > 0) {
+ realElement.setAttribute('page', page);
+ }
var fakeElement = editor.createFakeElement( realElement , 'cke_reference_footnote', 'hiddenfield', false );
editor.insertElement(fakeElement);
diff --git a/src/Plugin/Filter/ReferenceFootnotesFilter.php b/src/Plugin/Filter/ReferenceFootnotesFilter.php
index 7d6bc4d..1af1fae 100644
--- a/src/Plugin/Filter/ReferenceFootnotesFilter.php
+++ b/src/Plugin/Filter/ReferenceFootnotesFilter.php
@@ -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 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 tag?
+ if ($matches[1]) {
+ // See if value attribute can parsed, either well-formed in quotes eg
+ // .
+ if (preg_match('|' . $attribute . '=["\'](.*?)["\']|', $matches[1], $value_match)) {
+ $value = $value_match[1];
+ // Or without quotes eg .
+ }
+ elseif (preg_match('|' . $attribute . '=(\S*)|', $matches[1], $value_match)) {
+ $value = $value_match[1];
+ }
+ }
+ return $value;
+ }
}