3 namespace Drupal\filter\Plugin\Filter;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\Unicode;
7 use Drupal\Component\Utility\Xss;
8 use Drupal\filter\FilterProcessResult;
9 use Drupal\filter\Plugin\FilterBase;
10 use Drupal\filter\Render\FilteredMarkup;
13 * Provides a filter to caption elements.
15 * When used in combination with the filter_align filter, this must run last.
18 * id = "filter_caption",
19 * title = @Translation("Caption images"),
20 * description = @Translation("Uses a <code>data-caption</code> attribute on <code><img></code> tags to caption images."),
21 * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
24 class FilterCaption extends FilterBase {
29 public function process($text, $langcode) {
30 $result = new FilterProcessResult($text);
32 if (stristr($text, 'data-caption') !== FALSE) {
33 $dom = Html::load($text);
34 $xpath = new \DOMXPath($dom);
35 foreach ($xpath->query('//*[@data-caption]') as $node) {
36 // Read the data-caption attribute's value, then delete it.
37 $caption = Html::escape($node->getAttribute('data-caption'));
38 $node->removeAttribute('data-caption');
40 // Sanitize caption: decode HTML encoding, limit allowed HTML tags; only
41 // allow inline tags that are allowed by default, plus <br>.
42 $caption = Html::decodeEntities($caption);
43 $caption = FilteredMarkup::create(Xss::filter($caption, ['a', 'em', 'strong', 'cite', 'code', 'br']));
45 // The caption must be non-empty.
46 if (Unicode::strlen($caption) === 0) {
50 // Given the updated node and caption: re-render it with a caption, but
51 // bubble up the value of the class attribute of the captioned element,
52 // this allows it to collaborate with e.g. the filter_align filter.
53 $tag = $node->tagName;
54 $classes = $node->getAttribute('class');
55 $node->removeAttribute('class');
56 $node = ($node->parentNode->tagName === 'a') ? $node->parentNode : $node;
58 '#theme' => 'filter_caption',
59 // We pass the unsanitized string because this is a text format
60 // filter, and after filtering, we always assume the output is safe.
61 // @see \Drupal\filter\Element\ProcessedText::preRenderText()
62 '#node' => FilteredMarkup::create($node->C14N()),
64 '#caption' => $caption,
65 '#classes' => $classes,
67 $altered_html = \Drupal::service('renderer')->render($filter_caption);
69 // Load the altered HTML into a new DOMDocument and retrieve the element.
70 $updated_nodes = Html::load($altered_html)->getElementsByTagName('body')
74 foreach ($updated_nodes as $updated_node) {
75 // Import the updated node from the new DOMDocument into the original
76 // one, importing also the child nodes of the updated node.
77 $updated_node = $dom->importNode($updated_node, TRUE);
78 $node->parentNode->insertBefore($updated_node, $node);
80 // Finally, remove the original data-caption node.
81 $node->parentNode->removeChild($node);
84 $result->setProcessedText(Html::serialize($dom))
98 public function tips($long = FALSE) {
101 <p>You can caption images, videos, blockquotes, and so on. Examples:</p>
103 <li><code><img src="" data-caption="This is a caption" /></code></li>
104 <li><code><video src="" data-caption="The Drupal Dance" /></code></li>
105 <li><code><blockquote data-caption="Dries Buytaert">Drupal is awesome!</blockquote></code></li>
106 <li><code><code data-caption="Hello world in JavaScript.">alert("Hello world!");</code></code></li>
110 return $this->t('You can caption images (<code>data-caption="Text"</code>), but also videos, blockquotes, and so on.');