bibcite_footnotes-8.x-1.0-beta3/src/Plugin/Filter/ReferenceFootnotesFilter.php
src/Plugin/Filter/ReferenceFootnotesFilter.php
<?php
namespace Drupal\bibcite_footnotes\Plugin\Filter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Drupal\bibcite_footnotes\CitationTools;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* Reference Footnotes filter.
*
* @Filter(
* id = "filter_reference_footnotes",
* module = "bibcite_footnotes",
* title = @Translation("Inline citation filter"),
* description = @Translation("Convert bibcite-footnote tags to rendered citations."),
* type = \Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE,
* cache = FALSE,
* settings = {
* "enable_bidirectional_links" = TRUE,
* "backlink_symbol" = "↑",
* "backlink_position" = "after",
* "enable_bibliography_section" = TRUE,
* "bibliography_section_label" = "Bibliography",
* },
* weight = 0
* )
*/
class ReferenceFootnotesFilter extends FilterBase implements ContainerFactoryPluginInterface
{
/**
* The citation tools service.
*
* @var \Drupal\bibcite_footnotes\CitationTools
*/
protected $citationTools;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, CitationTools $citation_tools)
{
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->citationTools = $citation_tools;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition)
{
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('bibcite_footnotes.citation_tools')
);
}
/**
* {@inheritdoc}
*/
public function settings()
{
$settings = parent::settings();
// Ensure default values are set
$settings += [
'enable_bidirectional_links' => TRUE,
'backlink_symbol' => '↑',
'backlink_position' => 'after',
"enable_bibliography_section" => TRUE,
"bibliography_section_label" => $this->t("Bibliography"),
];
return $settings;
}
/**
* Create the settings form for the filter.
*
* @param array $form
* A minimally prepopulated form array.
* @param 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.
*/
public function settingsForm(array $form, FormStateInterface $form_state)
{
// Get the current settings with defaults
$settings = $this->settings;
$form['enable_bidirectional_links'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable bidirectional links between citations and bibliography'),
'#description' => $this->t('When enabled, citations will link to bibliography items and bibliography items will have backlinks to each citation.'),
'#default_value' => $settings['enable_bidirectional_links'],
];
$form['backlink_symbol'] = [
'#type' => 'textfield',
'#title' => $this->t('Backlink symbol'),
'#description' => $this->t('The symbol used for backlinks from bibliography to citations (e.g., ↑, ↖, ↩)'),
'#default_value' => $settings['backlink_symbol'],
'#size' => 10,
'#states' => [
'required' => [
':input[name="filters[filter_reference_footnotes][settings][enable_bidirectional_links]"]' => ['checked' => TRUE],
],
'visible' => [
':input[name="filters[filter_reference_footnotes][settings][enable_bidirectional_links]"]' => ['checked' => TRUE],
],
],
];
$form['backlink_position'] = [
'#type' => 'select',
'#title' => $this->t('Backlink position'),
'#description' => $this->t('Position of backlinks relative to bibliography entry.'),
'#options' => [
'before' => $this->t('Before entry'),
'after' => $this->t('After entry'),
],
'#default_value' => $settings['backlink_position'],
'#states' => [
'visible' => [
':input[name="filters[filter_reference_footnotes][settings][enable_bidirectional_links]"]' => ['checked' => TRUE],
],
],
];
$form['enable_bibliography_section'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable Bibliography section'),
'#description' => $this->t('When enabled, a Bibliography section will be printed at the bottom of the content.'
. '<br>Alternatively, the Works Cited Views style plugin can be used to render a bibliography section inside a block.'),
'#default_value' => $settings['enable_bibliography_section'],
];
$form['bibliography_section_label'] = [
'#type' => 'textfield',
'#title' => $this->t('Bibliography section label'),
'#description' => $this->t('The label for the inline bibliography section, rendered in an <h3> tag.'),
'#default_value' => $settings['bibliography_section_label'],
'#size' => 20,
'#states' => [
'required' => [
':input[name="filters[filter_reference_footnotes][settings][enable_bibliography_section]"]' => ['checked' => TRUE],
],
'visible' => [
':input[name="filters[filter_reference_footnotes][settings][enable_bibliography_section]"]' => ['checked' => TRUE],
],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function process($text, $langcode)
{
$result = new FilterProcessResult($text);
if (strpos($text, 'bibcite-footnote') === FALSE) {
return $result;
}
$dom = new \DOMDocument();
// Enable internal encoding handling
$dom->encoding = 'UTF-8';
// Load HTML without XML declaration
@$dom->loadHTML(
mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8'),
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
$xpath = new \DOMXPath($dom);
$footnotes = $xpath->query('//bibcite-footnote');
if ($footnotes->length === 0) {
return $result;
}
// Process all citations and remove invalid footnotes
$citationData = $this->processAllCitations($footnotes);
// Replace all valid footnotes with rendered citations
$this->replaceAllFootnotes($dom, $citationData['bibliography'], $citationData['citations']);
// Remove any remaining invalid footnotes
$this->removeInvalidFootnotes($dom);
$bibliography = array_values($citationData['bibliography']);
$citation_items = array_values($citationData['citations']);
// Add full bibliography section at the end of the content
$this->addBibliographySection($dom, $bibliography, $citation_items);
$result->setProcessedText($dom->saveHTML());
// Add cache context for filter settings
$result->addCacheableDependency($this);
return $result;
}
/**
* Process all footnote elements and prepare citation data
*/
private function processAllCitations(\DOMNodeList $footnotes): array
{
$citations = [];
$bibliography = [];
$citation_id_counts = [];
foreach ($footnotes as $footnote) {
$entity_id = $footnote->getAttribute('data-entity-id');
$page_range = $footnote->getAttribute('data-page-range');
if (empty($entity_id)) {
// Skip invalid footnotes (they will be removed later)
continue;
}
$citation = $this->citationTools->getRenderableReference($entity_id);
// Check if we got valid citation data
if (empty($citation['#data']) || empty($citation['#data']['citation-label'])) {
// Invalid reference, skip this footnote
continue;
}
$citation_label = $citation['#data']['citation-label'];
// Generate unique citation ID
$citation_count = $citation_id_counts[$citation_label] ?? 0;
$citation_id = $citation_label . '_' . $citation_count;
$citation['#data']['id'] = $citation_label;
// Store citation data
$citations[$citation_id] = [
'id' => $citation_label,
'locator' => $page_range,
'label' => 'page',
'fragment' => $footnote
];
if (empty($bibliography[$citation_label])) {
$bibliography[$citation_label] = $citation['#data'];
}
$citation_id_counts[$citation_label] = $citation_count + 1;
}
return [
'citations' => $citations,
'bibliography' => $bibliography
];
}
/**
* Replace all footnote elements with rendered citations
*/
private function replaceAllFootnotes(\DOMDocument $dom, array $bibliography, array $citations): void
{
$bibliography = array_values($bibliography);
foreach ($citations as $citation_id => $citation_data) {
$stripped_citation = $citation_data;
unset($stripped_citation['fragment']);
// Pass filter settings to the render function
$rendered_citation = $this->renderCitation(
$bibliography,
$stripped_citation,
$this->settings
);
$fragment = $dom->createDocumentFragment();
$fragment->appendXML('<span class="bibcite-citation">' . $rendered_citation . '</span>');
$citation_data['fragment']->parentNode->replaceChild($fragment, $citation_data['fragment']);
}
}
/**
* Remove invalid footnotes (those without entity-id or with invalid references)
*/
private function removeInvalidFootnotes(\DOMDocument $dom): void
{
$xpath = new \DOMXPath($dom);
$remainingFootnotes = $xpath->query('//bibcite-footnote');
foreach ($remainingFootnotes as $footnote) {
// Remove the footnote element from its parent
if ($footnote->parentNode) {
$footnote->parentNode->removeChild($footnote);
}
}
}
/**
* Add full bibliography section at the end of the content
*/
private function addBibliographySection(\DOMDocument $dom, array $bibliography, array $citation_list): void
{
if (empty($bibliography)) {
return;
}
// Create bibliography render array with filter settings
$bibliography_render = [
'#theme' => 'bibcite_footnotes_works_cited',
'#data' => $bibliography,
'#citation-items' => $citation_list,
'#filter_settings' => $this->settings, // Pass settings to theme
];
$rendered_bibliography = \Drupal::service('renderer')->render($bibliography_render);
// Create the footnotes section
if (empty($this->settings['enable_bibliography_section'])
|| $this->settings['enable_bibliography_section']) { // On by default for kernel tests.
$label = !empty($this->settings['bibliography_section_label']) ? $this->settings['bibliography_section_label'] : $this->t('Bibliography');
$footnotes_section = $dom->createDocumentFragment();
$footnotes_section->appendXML('
<div class="bibcite-footnotes-section">
<h3>' . $label .'</h3>
<div class="bibcite-bibliography">' . $rendered_bibliography . '</div>
</div>
');
// Use XPath to find the last element in the document
$xpath = new \DOMXPath($dom);
$allElements = $xpath->query('//*');
if ($allElements->length > 0) {
$lastElement = $allElements->item($allElements->length - 1);
$lastElement->parentNode->insertBefore($footnotes_section, $lastElement->nextSibling);
} else {
// Fallback: append to document element
$dom->documentElement->appendChild($footnotes_section);
}
}
}
/**
* Render a single citation with full bibliographical context
*/
private function renderCitation(array $bibliography, array $citation_items, array $filter_settings = []): string
{
$citation['#theme'] = 'bibcite_footnotes_citation';
$citation['#data'] = $bibliography;
$citation['#citation-items'] = [$citation_items];
$citation['#filter_settings'] = $filter_settings; // Pass settings to theme
return \Drupal::service('renderer')->render($citation);
}
/**
* {@inheritdoc}
*/
public function tips($long = FALSE)
{
return $this->t('Use the citation button in the editor to insert citations.');
}
}
