g2-8.x-1.x-dev/src/Plugin/Filter/Definition.php
src/Plugin/Filter/Definition.php
<?php
declare(strict_types=1);
namespace Drupal\g2\Plugin\Filter;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\filter\Attribute\Filter;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Drupal\filter\Plugin\FilterInterface;
use Drupal\g2\G2;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a filter to expand <dfn> entries to G2 glossary links.
*
* @Filter(
* id = "g2_definition",
* title = @Translation("Convert <code><dfn/></code> elements into links
* to definitions in the G2 glossary."), type =
* Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE,
* settings = {}, description=@Translation("Converts <code><dfn>some
* entry</dfn></code> elements into links to the matching G2 entry, or
* a homonyms disambiguation page if multiple entries match the given string.
* This filter <em>must</em> be applied after the automatic
* <code><dfn/></code> wrapping filter."), weight = 0,
* )
*
* @phpstan-consistent-constructor
*/
#[Filter(
id: "g2_definition",
title: new TranslatableMarkup("G2 Definition"),
// @todo Or TYPE_TRANSFORM_REVERSIBLE ?
type: FilterInterface::TYPE_MARKUP_LANGUAGE,
description: new TranslatableMarkup("Convert <code><dfn/></code> elements into links to definitions in the G2 glossary."),
weight: 0,
settings: [],
)]
class Definition extends FilterBase implements ContainerFactoryPluginInterface {
/**
* Cache metadata set and used during the ::process step.
*
* @var \Drupal\Core\Render\BubbleableMetadata|null
*/
protected ?BubbleableMetadata $metadata;
/**
* The config.factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* The entity_type.manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $etm;
/**
* The core renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
/**
* Constructor.
*
* @param array<string,mixed> $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param array<string,mixed> $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The core config.factory service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $etm
* The core entity_type.manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The core renderer service.
*/
public function __construct(
array $configuration,
string $plugin_id,
array $plugin_definition,
ConfigFactoryInterface $configFactory,
EntityTypeManagerInterface $etm,
RendererInterface $renderer,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $configFactory;
$this->etm = $etm;
$this->renderer = $renderer;
}
/**
* Static factory.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param array<string,mixed> $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param array<string,mixed> $plugin_definition
* The plugin definition.
*
* @return static
* The plugin instance.
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
): static {
$config = $container->get(G2::SVC_CONF);
$etm = $container->get(G2::SVC_ETM);
$renderer = $container->get('renderer');
return new static($configuration, $plugin_id, $plugin_definition, $config, $etm, $renderer);
}
/**
* {@inheritDoc}
*/
public function prepare($text, $langcode): string {
$text = parent::prepare($text, $langcode);
$text = preg_replace('@<dfn>(.+?)</dfn>@s', "[g2-dfn]\\1[/g2-dfn]",
$text);
if (empty($text)) {
return '';
}
assert(is_string($text));
return $text;
}
/**
* {@inheritDoc}
*/
public function process($text, $langcode) {
$settings = $this->configFactory->get(G2::CONFIG_NAME);
$target = $settings->get(G2::VARREMOTEG2);
// Arrow functions to prevent cloning $this in [$this, "doXXX"].
// The parentheses are for PHPCS not correctly handling arrow functions.
$method = empty($target)
/** @var array<string,string> $matches */
? (fn(array $matches): string => $this->doLocalProcess($matches))
: (fn(array $matches): string => $this->doRemoteProcess($matches));
$this->metadata = new BubbleableMetadata();
$text = preg_replace_callback('@\[g2-dfn\](.+?)\[/g2-dfn\]@s', $method, $text);
if (!is_string($text)) {
$text = '';
}
$result = new FilterProcessResult($text);
$result->addCacheableDependency($this->metadata);
unset($this->metadata);
return $result;
}
/**
* Translate glossary linking elements (<dfn>) to local links)
*
* This function generates absolute links, for the benefit of the WOTD RSS
* feed If this feed is not used, it is possible to use the (shorter)
* relative URLs by swapping comments.
*
* @param mixed[] $entry
* A 2-entry array, the first being the complete prepared text,
* and the second being its content.
*
* @return string
* HTML.
*/
protected function doLocalProcess(array $entry): string {
/** @var string $text */
$text = $entry[1] ?? '';
$tooltipsLevel = $this->configFactory->get(G2::CONFIG_NAME)
->get(G2::VARTOOLTIPS);
if ($tooltipsLevel === G2::TOOLTIPS_NONE) {
$tooltip = '';
}
else {
$nodes = $this->loadEntries($text);
$count = count($nodes);
switch ($count) {
case 0:
$tooltip = $this->t(
'No entry found for @entry', [
'@entry' => $text,
]
);
break;
case 1:
if ($tooltipsLevel == G2::TOOLTIPS_TITLES) {
/** @var \Drupal\node\NodeInterface $node */
$node = reset($nodes);
if (empty($this->metadata)) {
$this->metadata = new BubbleableMetadata();
}
$this->metadata->addCacheableDependency($node);
$tooltip = $node->label();
}
else {
$builder = $this->etm->getViewBuilder(G2::TYPE);
/** @var \Drupal\node\NodeInterface $node */
$node = reset($nodes);
if (empty($this->metadata)) {
$this->metadata = new BubbleableMetadata();
}
$this->metadata->addCacheableDependency($node);
$tooltipRA = $builder->view($node, G2::VM_TOOLTIPS);
$tooltipHTML = $this->renderer
->renderRoot($tooltipRA);
$tooltip = preg_replace('/(\s)\s+/m', '$1',
trim(strip_tags("$tooltipHTML")));
}
break;
default:
$tooltip = $this->formatPlural($count,
'@entry', '@count entries for @entry', [
'@count' => $count,
'@entry' => $text,
]
);
break;
}
}
$attributes = ['class' => 'g2-dfn-link'];
if (!empty($tooltip)) {
$attributes['title'] = $tooltip;
}
$link = Link::createFromRoute(
$text,
G2::ROUTE_HOMONYMS,
['g2_match' => $text],
['absolute' => TRUE, 'attributes' => $attributes]
);
return "{$link->toString()}";
}
/**
* Loader for G2_entries.
*
* @param string $title
* The title to look for.
*
* @return array<int,\Drupal\node\NodeInterface>
* Nodes matching the title.
*
* @see \Drupal\g2\Plugin\Filter\Definition::doProcess
*/
public function loadEntries(string $title) {
$storage = $this->etm->getStorage(G2::TYPE);
$nids = $storage->getQuery()
->accessCheck()
->condition('type', G2::BUNDLE)
->condition('status', NodeInterface::PUBLISHED)
->condition('title', $title)
->execute();
if (empty($nids)) {
return [];
}
$nodes = $storage->loadMultiple($nids);
return $nodes;
}
/**
* Translate glossary linking elements (<dfn>) to remote links)
*
* This function generates absolute links, for the benefit of the WOTD RSS
* feed If this feed is not used, it is possible to use the (shorter)
* relative URLs by swapping comments.
*
* @param mixed[] $entry
* A 2-entry array, the first being the complete prepared text,
* and the second being its content.
*
* @return string
* A <a href> string.
*/
protected function doRemoteProcess(array $entry): string {
/** @var string $text */
$text = $entry[1] ?? '';
// When this method is called, we know this is not empty.
$mixedTarget = $this->configFactory->get(G2::CONFIG_NAME)
->get(G2::VARREMOTEG2);
assert(is_scalar($mixedTarget) || $mixedTarget instanceof \Stringable);
$target = (string) $mixedTarget;
$path = urlencode(G2::encodeTerminal($text));
// We do not have access to the caching metadata on the remote site with
// the current version of the API.
$url = Url::fromUri("$target/$path", [
'absolute' => TRUE,
'attributes' => ['class' => 'g2-dfn-link'],
]);
$ret = Link::fromTextAndUrl($text, $url)->toString()->__toString();
return $ret;
}
/**
* {@inheritDoc}
*/
public function tips($long = FALSE): ?TranslatableMarkup {
$ret = $long
? $this->t('Wrap <dfn> elements around the terms for which you want a link to the available glossary entries. If you have enabled the automatic filter, you only have to do this for entries you placed on the glossary stop list.')
: $this->t('You may link to glossary entries manually using <dfn> elements. Especially useful for those on the stop list.');
return $ret;
}
}
