core/modules/filter/css/filter.caption-rtl.css | 18 ++ core/modules/filter/css/filter.caption.css | 37 ++++ core/modules/filter/filter.module | 23 +++ .../Drupal/filter/Plugin/Filter/FilterCaption.php | 116 ++++++++++++ .../lib/Drupal/filter/Tests/FilterUnitTest.php | 188 ++++++++++++++++---- .../filter/templates/filter-caption.html.twig | 18 ++ .../standard/config/filter.format.basic_html.yml | 5 + .../standard/config/filter.format.full_html.yml | 5 + core/themes/bartik/css/style.css | 28 +++ 9 files changed, 403 insertions(+), 35 deletions(-) diff --git a/core/modules/filter/css/filter.caption-rtl.css b/core/modules/filter/css/filter.caption-rtl.css new file mode 100644 index 0000000..7e43f31 --- /dev/null +++ b/core/modules/filter/css/filter.caption-rtl.css @@ -0,0 +1,18 @@ +/** + * @file + * Caption filter: RTL styling for displaying image captions. + */ + +/** + * Caption alignment. + */ +.caption-left { + float: right; + margin-left: auto; + margin-right: 0; +} +.caption-right { + float: left; + margin-left: 0; + margin-right: auto; +} diff --git a/core/modules/filter/css/filter.caption.css b/core/modules/filter/css/filter.caption.css new file mode 100644 index 0000000..2c6c059 --- /dev/null +++ b/core/modules/filter/css/filter.caption.css @@ -0,0 +1,37 @@ +/** + * @file + * Caption filter: default styling for displaying image captions. + */ + +/** + * Essentials, based on http://stackoverflow.com/a/13363408. + */ +.caption { + display: table; +} +.caption > * { + display: block; + max-width: 100%; +} +.caption > figcaption { + display: table-caption; + caption-side: bottom; + max-width: none; +} + +/** + * Caption alignment. + */ +.caption-left { + float: left; /* LTR */ + margin-left: 0; /* LTR */ +} +.caption-right { + float: right; /* LTR */ + margin-right: 0; /* LTR */ +} +.caption-center { + margin-left: auto; + margin-right: auto; + text-align: center; +} diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index 263afb5..1a8e679 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -89,6 +89,15 @@ function filter_theme() { 'filter_html_image_secure_image' => array( 'variables' => array('image' => NULL), ), + 'filter_caption' => array( + 'variables' => array( + 'node' => NULL, + 'tag' => NULL, + 'caption' => NULL, + 'align' => NULL, + ), + 'template' => 'filter-caption', + ) ); } @@ -1449,6 +1458,13 @@ function theme_filter_html_image_secure_image(&$variables) { */ /** + * Implements hook_page_build(). + */ +function filter_page_build(&$page) { + $page['#attached']['library'][] = array('filter', 'caption'); +} + +/** * Implements hook_library_info(). */ function filter_library_info() { @@ -1497,6 +1513,13 @@ function filter_library_info() { array('system', 'jquery.once'), ), ); + $libraries['caption'] = array( + 'title' => 'Captions for images and alignments', + 'version' => VERSION, + 'css' => array( + $path . '/css/filter.caption.css', + ), + ); return $libraries; } diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterCaption.php b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterCaption.php new file mode 100644 index 0000000..48fb5e1 --- /dev/null +++ b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterCaption.php @@ -0,0 +1,116 @@ +query('//*[@data-caption or @data-align]') as $node) { + $caption = NULL; + $align = NULL; + + // Retrieve, then remove the data-caption and data-align attributes. + if ($node->hasAttribute('data-caption')) { + $caption = String::checkPlain($node->getAttribute('data-caption')); + $node->removeAttribute('data-caption'); + // Sanitize caption: decode HTML encoding, limit allowed HTML tags. + $caption = String::decodeEntities($caption); + $caption = Xss::filter($caption); + // The caption must be non-empty. + if (Unicode::strlen($caption) === 0) { + $caption = NULL; + } + } + if ($node->hasAttribute('data-align')) { + $align = $node->getAttribute('data-align'); + $node->removeAttribute('data-align'); + // Only allow 3 values: 'left', 'center' and 'right'. + if (!in_array($align, array('left', 'center', 'right'))) { + $align = NULL; + } + } + + // If neither attribute has a value after validation, then don't + // transform the HTML. + if ($caption === NULL && $align === NULL) { + continue; + } + + // Given the updated node, caption and alignment: re-render it with a + // caption. + $altered_html = theme('filter_caption', array( + 'node' => $node->C14N(), + 'tag' => $node->tagName, + 'caption' => $caption, + 'align' => $align, + )); + + // Load the altered HTML into a new DOMDocument and retrieve the element. + $updated_node = filter_dom_load($altered_html)->getElementsByTagName('body') + ->item(0) + ->childNodes + ->item(0); + + // Import the updated node from the new DOMDocument into the original + // one, importing also the child nodes of the updated node. + $updated_node = $dom->importNode($updated_node, TRUE); + // Finally, replace the original image node with the new image node! + $node->parentNode->replaceChild($updated_node, $node); + } + + return filter_dom_serialize($dom); + } + + return $text; + } + + /** + * {@inheritdoc} + */ + public function tips($long = FALSE) { + if ($long) { + return t(' +
You can add image captions and align images left, right or centered. Examples:
+<img src="" data-caption="This is a caption" />
<img src="" data-align="center" />
<img src="" data-caption="Alpaca" data-align="right" />
-
-
- ',
- 'filter_html_help' => 1,
- 'filter_html_nofollow' => 0,
- );
+ // Get FilterHtml object.
+ $filter = $this->filters['filter_html'];
+ $filter->setPluginConfiguration(array(
+ 'settings' => array(
+ 'allowed_html' => '
-
-
- ',
+ 'filter_html_help' => 1,
+ 'filter_html_nofollow' => 0,
+ )
+ ));
// HTML filter is not able to secure some tags, these should never be
// allowed.
@@ -173,13 +283,15 @@ function testHtmlFilter() {
* Tests the spam deterrent.
*/
function testNoFollowFilter() {
- // Setup dummy filter object.
- $filter = new stdClass();
- $filter->settings = array(
- 'allowed_html' => '',
- 'filter_html_help' => 1,
- 'filter_html_nofollow' => 1,
- );
+ // Get FilterHtml object.
+ $filter = $this->filters['filter_html'];
+ $filter->setPluginConfiguration(array(
+ 'settings' => array(
+ 'allowed_html' => '',
+ 'filter_html_help' => 1,
+ 'filter_html_nofollow' => 1,
+ )
+ ));
// Test if the rel="nofollow" attribute is added, even if we try to prevent
// it.
@@ -206,9 +318,8 @@ function testNoFollowFilter() {
* check_plain() is not tested here.
*/
function testHtmlEscapeFilter() {
- // Setup dummy filter object.
- $filter = new stdClass();
- $filter->callback = '_filter_html_escape';
+ // Get FilterHtmlEscape object.
+ $filter = $this->filters['filter_html_escape'];
$tests = array(
" One. Two'.\n
Three.
\n " => array(
@@ -224,12 +335,14 @@ function testHtmlEscapeFilter() {
* Tests the URL filter.
*/
function testUrlFilter() {
- // Setup dummy filter object.
- $filter = new stdClass();
- $filter->callback = '_filter_url';
- $filter->settings = array(
- 'filter_url_length' => 496,
- );
+ // Get FilterUrl object.
+ $filter = $this->filters['filter_url'];
+ $filter->setPluginConfiguration(array(
+ 'settings' => array(
+ 'filter_url_length' => 496,
+ )
+ ));
+
// @todo Possible categories:
// - absolute, mail, partial
// - characters/encoding, surrounding markup, security
@@ -516,7 +629,11 @@ function testUrlFilter() {
$this->assertFilteredString($filter, $tests);
// URL trimming.
- $filter->settings['filter_url_length'] = 20;
+ $filter->setPluginConfiguration(array(
+ 'settings' => array(
+ 'filter_url_length' => 20,
+ )
+ ));
$tests = array(
'www.trimmed.com/d/ff.ext?a=1&b=2#a1' => array(
'www.trimmed.com/d/ff...' => TRUE,
@@ -528,7 +645,7 @@ function testUrlFilter() {
/**
* Asserts multiple filter output expectations for multiple input strings.
*
- * @param $filter
+ * @param FilterInterface $filter
* A input filter object.
* @param $tests
* An associative array, whereas each key is an arbitrary input string and
@@ -548,8 +665,7 @@ function testUrlFilter() {
*/
function assertFilteredString($filter, $tests) {
foreach ($tests as $source => $tasks) {
- $function = $filter->callback;
- $result = $function($source, $filter);
+ $result = $filter->process($source, $filter, FALSE, '');
foreach ($tasks as $value => $is_expected) {
// Not using assertIdentical, since combination with strpos() is hard to grok.
if ($is_expected) {
@@ -593,11 +709,13 @@ function assertFilteredString($filter, $tests) {
* - Mix of absolute and partial URLs, and e-mail addresses in one content.
*/
function testUrlFilterContent() {
- // Setup dummy filter object.
- $filter = new stdClass();
- $filter->settings = array(
- 'filter_url_length' => 496,
- );
+ // Get FilterUrl object.
+ $filter = $this->filters['filter_url'];
+ $filter->setPluginConfiguration(array(
+ 'settings' => array(
+ 'filter_url_length' => 496,
+ )
+ ));
$path = drupal_get_path('module', 'filter') . '/tests';
$input = file_get_contents($path . '/filter.url-input.txt');
diff --git a/core/modules/filter/templates/filter-caption.html.twig b/core/modules/filter/templates/filter-caption.html.twig
new file mode 100644
index 0000000..532fb37
--- /dev/null
+++ b/core/modules/filter/templates/filter-caption.html.twig
@@ -0,0 +1,18 @@
+{#
+/**
+ * Returns HTML for a captioned image, audio, video or other tag.
+ *
+ * Available variables
+ * - string node: The complete HTML tag whose contents are being captioned.
+ * - string tag: The name of the HTML tag whose contents are being captioned.
+ * - string|NULL caption: (optional) The caption text, or NULL.
+ * - string|NULL align: (optional) The alignment: 'left', 'center', 'right' or
+ * NULL.
+ */
+#}
+
diff --git a/core/profiles/standard/config/filter.format.basic_html.yml b/core/profiles/standard/config/filter.format.basic_html.yml
index 5810786..69fc520 100644
--- a/core/profiles/standard/config/filter.format.basic_html.yml
+++ b/core/profiles/standard/config/filter.format.basic_html.yml
@@ -14,6 +14,11 @@ filters:
allowed_html: ' -
-
-
'
filter_html_help: '0'
filter_html_nofollow: '0'
+ filter_caption:
+ module: filter
+ status: '1'
+ weight: '8'
+ settings: { }
filter_html_image_secure:
module: filter
status: '1'
diff --git a/core/profiles/standard/config/filter.format.full_html.yml b/core/profiles/standard/config/filter.format.full_html.yml
index 204a342..0265845 100644
--- a/core/profiles/standard/config/filter.format.full_html.yml
+++ b/core/profiles/standard/config/filter.format.full_html.yml
@@ -6,6 +6,11 @@ roles:
- administrator
cache: '1'
filters:
+ filter_caption:
+ module: filter
+ status: '1'
+ weight: '9'
+ settings: { }
filter_htmlcorrector:
module: filter
status: '1'
diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css
index a7a36a8..640bca3 100644
--- a/core/themes/bartik/css/style.css
+++ b/core/themes/bartik/css/style.css
@@ -1417,6 +1417,34 @@ ol.search-results {
padding-left: 0;
}
+/* -------------- Captions -------------- */
+
+.caption > * {
+ background: #F3F3F3;
+ padding: 0.5ex;
+ border: 1px solid #CCC;
+}
+
+.caption > figcaption {
+ border: 1px solid #CCC;
+ border-top: none;
+ padding-top: 0.5ex;
+ font-size: small;
+ text-align: center;
+}
+
+/* Override Bartik's default blockquote and pre styles when captioned. */
+.caption-pre > pre,
+.caption-blockquote > blockquote {
+ margin: 0;
+}
+.caption-blockquote > figcaption::before {
+ content: "— ";
+}
+.caption-blockquote > figcaption {
+ text-align: left;
+}
+
/* -------------- Shortcut Links -------------- */
.shortcut-wrapper {