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; + } }