class FootnotesFilter in Footnotes 8.2
Provides a base filter for Footnotes filter.
Plugin annotation
@Filter(
id = "filter_footnotes",
module = "footnotes",
title = @Translation("Footnotes filter"),
description = @Translation("You can insert footnotes directly into texts."),
type = \Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE,
cache = FALSE,
settings = {
"footnotes_collapse" = FALSE,
"footnotes_html" = FALSE
},
weight = 0
)
Hierarchy
- class \Drupal\Component\Plugin\PluginBase implements DerivativeInspectionInterface, PluginInspectionInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
- class \Drupal\filter\Plugin\FilterBase implements FilterInterface
- class \Drupal\footnotes\Plugin\Filter\FootnotesFilter uses StringTranslationTrait
- class \Drupal\filter\Plugin\FilterBase implements FilterInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
Expanded class hierarchy of FootnotesFilter
File
- src/
Plugin/ Filter/ FootnotesFilter.php, line 28
Namespace
Drupal\footnotes\Plugin\FilterView source
class FootnotesFilter extends FilterBase {
use StringTranslationTrait;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a FootnotesFilter object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->renderer = \Drupal::service('renderer');
}
/**
* Get the tips for the filter.
*
* @param bool $long
* If get the long or short tip.
*
* @return string
* The tip to show for the user.
*/
public function tips($long = FALSE) {
if ($long) {
return $this
->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 <a href=":link">Footnotes Readme page</a> for additional usage options.', [
':link' => 'http://drupal.org/project/footnotes">',
]);
}
else {
return $this
->t('Use [fn]...[/fn] (or <fn>...</fn>) to insert automatically numbered footnotes.');
}
}
/**
* {@inheritdoc}
*/
public function process($text, $langcode) {
// 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).
if (is_array($text)) {
implode($text);
}
$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 = [];
$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($this
->t("You have unclosed fn tags. This is invalid and will\n produce unpredictable results."));
}
// Before doing the replacement, the callback function needs to know which
// options to use.
$this
->replaceCallback($this->settings, 'prepare');
$pattern = '|<fn([^>]*)>(.*?)</fn>|s';
$text = preg_replace_callback($pattern, [
$this,
'replaceCallback',
], $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 = $this
->replaceCallback(NULL, 'output footer');
$pattern = '|(<footnotes([\\ \\/]*)>)|';
if (preg_match($pattern, $text) > 0) {
$text = preg_replace($pattern, $footer, $text, 1);
}
elseif (!empty($footer)) {
$text .= "\n\n" . $footer;
}
$result = new FilterProcessResult($text);
$result
->setAttachments([
'library' => [
'footnotes/footnotes',
],
]);
return $result;
}
/**
* 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 mixed $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 $opt_html = 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['footnotes_collapse'];
$opt_html = $matches['footnotes_html'];
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 = $this->renderer
->render($markup);
}
// 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 = '';
// 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 = $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);
// 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' => $opt_html ? html_entity_decode($matches[2]) : $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 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 = $this->renderer
->render($fn);
return $result;
}
/**
* Helper function to return a random text string.
*
* @return string
* Random (lowercase) alphanumeric string.
*/
public function randstr() {
$chars = "abcdefghijklmnopqrstuwxyz1234567890";
$str = "";
// Seeding with srand() not necessary in modern PHP versions.
for ($i = 0; $i < 7; $i++) {
$n = rand(0, strlen($chars) - 1);
$str .= substr($chars, $n, 1);
}
return $str;
}
/**
* Create the settings form for the filter.
*
* @param array $form
* A minimally prepopulated form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the (entire) configuration form.
*
* @return array
* The $form array with additional form elements for the settings of
* this filter. The submitted form values should match $this->settings.
*
* @todo Add validation of submited form values, it already exists for
* drupal 7, must update it only.
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$settings['footnotes_collapse'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Collapse footnotes with identical content'),
'#default_value' => $this->settings['footnotes_collapse'],
'#description' => $this
->t('If two footnotes have the exact same content, they will be collapsed into one as if using the same value="" attribute.'),
];
$settings['footnotes_html'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Handle footnote text as HTML'),
'#default_value' => $this->settings['footnotes_html'],
'#description' => $this
->t('If not checked, a HTML tag in the footnote text will be shown as-is to the user.'),
];
return $settings;
}
/**
* 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, array &$store_matches) {
if (!empty($store_matches)) {
foreach ($store_matches as &$fn) {
if ($fn['text'] == $text) {
return $fn['value'];
}
}
}
return FALSE;
}
}
Members
Name![]() |
Modifiers | Type | Description | Overrides |
---|---|---|---|---|
DependencySerializationTrait:: |
protected | property | An array of entity type IDs keyed by the property name of their storages. | |
DependencySerializationTrait:: |
protected | property | An array of service IDs keyed by property name used for serialization. | |
DependencySerializationTrait:: |
public | function | 1 | |
DependencySerializationTrait:: |
public | function | 2 | |
FilterBase:: |
public | property | The name of the provider that owns this filter. | |
FilterBase:: |
public | property | An associative array containing the configured settings of this filter. | |
FilterBase:: |
public | property | A Boolean indicating whether this filter is enabled. | |
FilterBase:: |
public | property | The weight of this filter compared to others in a filter collection. | |
FilterBase:: |
public | function |
Calculates dependencies for the configured plugin. Overrides DependentPluginInterface:: |
1 |
FilterBase:: |
public | function |
Gets default configuration for this plugin. Overrides ConfigurableInterface:: |
|
FilterBase:: |
public | function |
Gets this plugin's configuration. Overrides ConfigurableInterface:: |
|
FilterBase:: |
public | function |
Returns the administrative description for this filter plugin. Overrides FilterInterface:: |
|
FilterBase:: |
public | function |
Returns HTML allowed by this filter's configuration. Overrides FilterInterface:: |
4 |
FilterBase:: |
public | function |
Returns the administrative label for this filter plugin. Overrides FilterInterface:: |
|
FilterBase:: |
public | function |
Returns the processing type of this filter plugin. Overrides FilterInterface:: |
|
FilterBase:: |
public | function |
Prepares the text for processing. Overrides FilterInterface:: |
|
FilterBase:: |
public | function |
Sets the configuration for this plugin instance. Overrides ConfigurableInterface:: |
1 |
FilterInterface:: |
constant | HTML tag and attribute restricting filters to prevent XSS attacks. | ||
FilterInterface:: |
constant | Non-HTML markup language filters that generate HTML. | ||
FilterInterface:: |
constant | Irreversible transformation filters. | ||
FilterInterface:: |
constant | Reversible transformation filters. | ||
FootnotesFilter:: |
protected | property | The renderer. | |
FootnotesFilter:: |
private | function | Search the $store_matches array for footnote text that matches. | |
FootnotesFilter:: |
public | function |
Performs the filter processing. Overrides FilterInterface:: |
|
FootnotesFilter:: |
public | function | Helper function to return a random text string. | |
FootnotesFilter:: |
protected | function | Helper function called from preg_replace_callback() above. | |
FootnotesFilter:: |
public | function |
Create the settings form for the filter. Overrides FilterBase:: |
|
FootnotesFilter:: |
public | function |
Get the tips for the filter. Overrides FilterBase:: |
|
FootnotesFilter:: |
public | function |
Constructs a FootnotesFilter object. Overrides FilterBase:: |
|
MessengerTrait:: |
protected | property | The messenger. | 29 |
MessengerTrait:: |
public | function | Gets the messenger. | 29 |
MessengerTrait:: |
public | function | Sets the messenger. | |
PluginBase:: |
protected | property | Configuration information passed into the plugin. | 1 |
PluginBase:: |
protected | property | The plugin implementation definition. | 1 |
PluginBase:: |
protected | property | The plugin_id. | |
PluginBase:: |
constant | A string which is used to separate base plugin IDs from the derivative ID. | ||
PluginBase:: |
public | function |
Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the definition of the plugin implementation. Overrides PluginInspectionInterface:: |
3 |
PluginBase:: |
public | function |
Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface:: |
|
PluginBase:: |
public | function | Determines if the plugin is configurable. | |
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |