View source
<?php
namespace Drupal\onomasticon\Plugin\Filter;
use Drupal\Core\Config\Schema\ArrayElement;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\Entity\Term;
use Masterminds\HTML5;
class FilterOnomasticon extends FilterBase {
private $htmlDom;
private $htmlTree = array();
private $processedPaths = array();
private $htmlReplacements = array();
private $terms = array();
private $termCache = array();
public function process($text, $langcode) {
if (empty($this->settings['onomasticon_vocabulary'])) {
return $text;
}
$html5 = new HTML5();
$this->htmlDom = $html5
->loadHTML('<html><body>' . $text . '</body></html>');
$this->htmlDom->preserveWhiteSpace = false;
$this->htmlDom
->normalizeDocument();
$rootElement = $this->htmlDom->documentElement;
$body = $rootElement
->getElementsByTagName('body')
->item(0);
$this
->processChildren($body);
foreach ($this->htmlReplacements as $r) {
$domFragment = $this->htmlDom
->createDocumentFragment();
try {
$bool = $domFragment
->appendXML(self::html_entities_normalize_xml($r['html']));
} catch (Exception $e) {
$bool = FALSE;
}
if ($bool) {
$r['dom']->parentNode
->replaceChild($domFragment, $r['dom']);
}
}
$text = '';
foreach ($body->childNodes as $childNode) {
$text .= $body->ownerDocument
->saveHTML($childNode);
}
$result = new FilterProcessResult($text);
return $result;
}
private function html_entities_normalize_xml($string) {
$string = html_entity_decode(stripslashes($string), ENT_QUOTES, 'UTF-8');
$result = '';
$ar = preg_split('/(?<!^)(?!$)/u', $string);
foreach ($ar as $c) {
$o = ord($c);
if (strlen($c) > 1 || ($o < 32 || $o > 126) || $o == 38) {
$c = mb_encode_numericentity($c, array(
0x0,
0xffff,
0,
0xffff,
), 'UTF-8');
}
$result .= $c;
}
$result = str_replace('&', '###amp###', $result);
$result = html_entity_decode($result, ENT_HTML5);
$result = str_replace('###amp###', '&', $result);
return $result;
}
public function processChildren($dom) {
if ($dom
->hasChildNodes()) {
$this->htmlTree[] = $dom->nodeName;
foreach ($dom->childNodes as $child) {
$this
->processChildren($child);
}
}
else {
if ($dom->nodeName == '#text' && !$dom
->isWhitespaceInElementContent()) {
$disabled_tags = explode(' ', $this->settings['onomasticon_disabled']);
$disabled_tags = array_map(function ($tag) {
return preg_replace("/[^a-z1-6]*/", "", strtolower(trim($tag)));
}, $disabled_tags);
$disabled_tags[] = $this->settings['onomasticon_tag'];
$disabled_tags[] = 'a';
$disabled_tags[] = 'nonomasticon';
$disabled_tags = array_unique($disabled_tags);
$bad_tags = array_intersect($disabled_tags, $this->htmlTree);
if (count($bad_tags) == 0) {
if (!in_array($dom
->getNodePath(), $this->processedPaths)) {
$text_orig = $dom->nodeValue;
$text_repl = $this
->replaceTerms($text_orig);
if ($text_orig !== $text_repl) {
$this->htmlReplacements[] = array(
'dom' => $dom,
'html' => $text_repl,
);
}
$this->processedPaths[] = $dom
->getNodePath();
}
}
}
if (empty($dom->nextSibling)) {
array_pop($this->htmlTree);
$parent = $dom;
while (!empty($parent->parentNode) && empty($parent->nextSibling) && count($this->htmlTree) > 0) {
array_pop($this->htmlTree);
$parent = $parent->parentNode;
}
}
}
}
public function replaceTerms($text) {
if ($this->settings['onomasticon_repetition']) {
$preg_limit = 1;
}
else {
$preg_limit = -1;
}
$replacements = array();
$language = \Drupal::languageManager()
->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
->getId();
$terms = $this
->getTaxonomyTerms();
if (is_array($terms) && count($terms) > 0) {
foreach ($this
->getTaxonomyTerms() as $term) {
if ($this->settings['onomasticon_repetition'] && array_key_exists($term
->id(), $this->termCache)) {
continue;
}
if (!$term
->hasTranslation($language)) {
continue;
}
$term = $term
->getTranslation($language);
$term_name = $term
->label();
if ($this->settings['onomasticon_ignorecase']) {
$pos = strpos(strtolower($text), strtolower($term_name));
}
else {
$pos = strpos($text, $term_name);
if ($pos === FALSE) {
$pos = strpos($text, ucfirst($term_name));
}
}
if ($pos === FALSE) {
continue;
}
$needle = substr($text, $pos, strlen($term_name));
if (!array_key_exists($term
->id(), $this->termCache)) {
$this->termCache[$term
->id()] = TRUE;
}
$description = $term
->getDescription();
$implement = $this->settings['onomasticon_implement'];
if ($implement == 'attr_title') {
$description = strip_tags($description);
$description = str_replace('"', '', $description);
$description = str_replace(' ', ' ', $description);
$description = preg_replace('/\\s+/m', ' ', $description);
}
$aliasManager = \Drupal::service('path_alias.manager');
$termpath = $aliasManager
->getAliasByPath('/taxonomy/term/' . $term
->id());
$onomasticon = [
'#theme' => 'onomasticon',
'#tag' => $this->settings['onomasticon_tag'],
'#needle' => $needle,
'#description' => $description,
'#implement' => $implement,
'#orientation' => $this->settings['onomasticon_orientation'],
'#cursor' => $this->settings['onomasticon_cursor'],
'#termlink' => $this->settings['onomasticon_termlink'],
'#termpath' => $termpath,
];
$placeholder = '###' . $term
->id() . '###';
$replacements[$placeholder] = trim(\Drupal::service('renderer')
->render($onomasticon));
$text = preg_replace("/(?<![a-zA-Z0-9_äöüÄÖÜ])" . preg_quote($needle, '/') . "(?![a-zA-Z0-9_äöüÄÖÜ])/", $placeholder, $text, $preg_limit);
}
foreach ($replacements as $placeholder => $replacement) {
$text = str_replace($placeholder, $replacement, $text, $preg_limit);
}
}
return $text;
}
private function getTaxonomyTerms() {
if (empty($this->terms)) {
$terms = \Drupal::entityTypeManager()
->getStorage('taxonomy_term')
->loadTree($this->settings['onomasticon_vocabulary'], 0, NULL, true);
}
\Drupal::moduleHandler()
->alter('onomasticon_terms', $terms);
if (is_array($terms)) {
$this->terms = $terms;
}
return $this->terms;
}
public function settingsForm(array $form, FormStateInterface $form_state) {
$vocabularies = Vocabulary::loadMultiple();
$options = array();
foreach ($vocabularies as $vocabulary) {
$options[$vocabulary
->id()] = $vocabulary
->get('name');
}
$form['onomasticon_vocabulary'] = array(
'#type' => 'select',
'#title' => $this
->t('Vocabulary'),
'#options' => $options,
'#default_value' => $this->settings['onomasticon_vocabulary'],
'#description' => $this
->t('Choose the vocabulary that holds the glossary terms.'),
);
$form['onomasticon_tag'] = array(
'#type' => 'select',
'#title' => $this
->t('HTML tag'),
'#options' => array(
'dfn' => $this
->t('Definition (dfn)'),
'abbr' => $this
->t('Abbreviation (abbr)'),
'cite' => $this
->t('Title of work (cite)'),
),
'#default_value' => $this->settings['onomasticon_tag'],
'#description' => $this
->t('Choose the HTML tag to contain the glossary term.'),
);
$form['onomasticon_disabled'] = array(
'#type' => 'textfield',
'#title' => $this
->t('Disabled tags'),
'#default_value' => $this->settings['onomasticon_disabled'],
'#description' => $this
->t('Enter all HTML elements in which terms should not be replaced. Anchor tag as well as the default HTML tag are added to that list automatically.'),
);
$form['onomasticon_implement'] = array(
'#type' => 'select',
'#title' => $this
->t('Implementation'),
'#options' => array(
'extra_element' => $this
->t('Extra element'),
'attr_title' => $this
->t('Title attribute'),
),
'#default_value' => $this->settings['onomasticon_implement'],
'#description' => $this
->t('Choose the implementation of the glossary term description. Due to HTML convention, the description will be stripped of any tags as they are not allowed in a tag\'s attribute.'),
);
$form['onomasticon_orientation'] = array(
'#type' => 'select',
'#title' => $this
->t('Orientation'),
'#options' => array(
'above' => $this
->t('Above'),
'below' => $this
->t('Below'),
),
'#default_value' => $this->settings['onomasticon_orientation'],
'#description' => $this
->t('Choose whether the tooltip should appear above or below the hovered glossary term.'),
'#states' => array(
'visible' => array(
'select[name="filters[filter_onomasticon][settings][onomasticon_implement]"]' => array(
'value' => 'extra_element',
),
),
),
);
$form['onomasticon_cursor'] = array(
'#type' => 'select',
'#title' => $this
->t('Mouse cursor'),
'#options' => array(
'default' => $this
->t('Default (Text cursor)'),
'help' => $this
->t('Help cursor'),
'none' => $this
->t('Hide cursor'),
),
'#default_value' => $this->settings['onomasticon_cursor'],
'#description' => $this
->t('Choose a style the mouse cursor will change to when hovering a glossary term.'),
);
$form['onomasticon_ignorecase'] = array(
'#type' => 'checkbox',
'#title' => $this
->t('Ignore case'),
'#default_value' => $this->settings['onomasticon_ignorecase'],
'#description' => $this
->t('If checked, Onomasticon will find all occurrences of a term regardless of case (even CamelCase will work). If not checked, Onomasticon will only find the exact cased term or with the first letter capitalized (i.e. for start of sentences).'),
);
$form['onomasticon_repetition'] = array(
'#type' => 'checkbox',
'#title' => $this
->t('Add definition to first occurrence of term in text, only.'),
'#default_value' => $this->settings['onomasticon_repetition'],
'#description' => $this
->t('Disable this option to add definitions to all occurrences in text. This option\'s scope is a single text area.'),
);
$form['onomasticon_termlink'] = array(
'#type' => 'checkbox',
'#title' => $this
->t('Add a link to the term entity.'),
'#default_value' => $this->settings['onomasticon_termlink'],
'#description' => $this
->t('If you enable this option the tooltip will be extended by a link to the full term entity which enables you to show even more information. This only works if the implementation is done with an extra element.'),
);
return $form;
}
}