Browse Source

Support parenthetical citaiotns with page numbers.

mla_citations_3
Alexander O'Neill 6 years ago
parent
commit
7c967b72de
  1. 34
      bibcite_footnotes.module
  2. 15
      js/plugins/reference_footnotes/dialogs/footnotes.js
  3. 9
      js/plugins/reference_footnotes/plugin.js
  4. 206
      src/Plugin/Filter/ReferenceFootnotesFilter.php

34
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'] = '<h2>Hello!</h2>';
$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) {

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

9
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('<fn></fn>');
}
@ -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);

206
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 <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('"', "&quot;", $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;
}
}

Loading…
Cancel
Save