Issue #2014895 by Wim Leers, jessebeach: Image captions & alignment.
parent
6e4b41358c
commit
500c0820e6
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* @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 */
|
||||
}
|
||||
[dir=rtl] .caption-left {
|
||||
float: right;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
.caption-right {
|
||||
float: right; /* LTR */
|
||||
margin-right: 0; /* LTR */
|
||||
}
|
||||
[dir=rtl] .caption-right {
|
||||
float: left;
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
.caption-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
|
@ -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',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1576,6 +1585,13 @@ function theme_filter_html_image_secure_image(&$variables) {
|
|||
* @} End of "defgroup standard_filters".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_page_build().
|
||||
*/
|
||||
function filter_page_build(&$page) {
|
||||
$page['#attached']['library'][] = array('filter', 'caption');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_library_info().
|
||||
*/
|
||||
|
@ -1625,6 +1641,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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\filter\Plugin\Filter\FilterCaption.
|
||||
*/
|
||||
|
||||
namespace Drupal\filter\Plugin\Filter;
|
||||
|
||||
use Drupal\Component\Utility\String;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\Component\Utility\Xss;
|
||||
use Drupal\Core\Annotation\Translation;
|
||||
use Drupal\filter\Annotation\Filter;
|
||||
use Drupal\filter\Plugin\FilterBase;
|
||||
|
||||
/**
|
||||
* Provides a filter to display image captions and align images.
|
||||
*
|
||||
* @Filter(
|
||||
* id = "filter_caption",
|
||||
* module = "filter",
|
||||
* title = @Translation("Display image captions and align images"),
|
||||
* description = @Translation("Uses data-caption and data-align attributes on <img> tags to caption and align images."),
|
||||
* type = FILTER_TYPE_TRANSFORM_REVERSIBLE
|
||||
* )
|
||||
*/
|
||||
class FilterCaption extends FilterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process($text, $langcode, $cache, $cache_id) {
|
||||
$search = array();
|
||||
$replace = array();
|
||||
|
||||
if (stristr($text, 'data-caption') !== FALSE || stristr($text, 'data-align') !== FALSE) {
|
||||
$dom = filter_dom_load($text);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
foreach ($xpath->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('
|
||||
<p>You can add image captions and align images left, right or centered. Examples:</p>
|
||||
<ul>
|
||||
<li>Caption an image: <code><img src="" data-caption="This is a caption" /></code></li>
|
||||
<li>Align an image: <code><img src="" data-align="center" /></code></li>
|
||||
<li>Caption & align an image: <code><img src="" data-caption="Alpaca" data-align="right" /></code></li>
|
||||
</ul>');
|
||||
}
|
||||
else {
|
||||
return t('You can caption (data-caption="Text") and align images (data-align="center"), but also video, blockquotes, and so on.');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ namespace Drupal\filter\Tests;
|
|||
|
||||
use Drupal\simpletest\DrupalUnitTestBase;
|
||||
use stdClass;
|
||||
use Drupal\filter\FilterBag;
|
||||
use Drupal\filter\Plugin\Filter\FilterCaption;
|
||||
|
||||
/**
|
||||
* Unit tests for core filters.
|
||||
|
@ -33,15 +35,121 @@ class FilterUnitTest extends DrupalUnitTestBase {
|
|||
protected function setUp() {
|
||||
parent::setUp();
|
||||
config_install_default_config('module', 'system');
|
||||
|
||||
$manager = $this->container->get('plugin.manager.filter');
|
||||
$bag = new FilterBag($manager, array());
|
||||
$this->filters = $bag->getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the caption filter.
|
||||
*/
|
||||
function testCaptionFilter() {
|
||||
$filter = $this->filters['filter_caption'];
|
||||
|
||||
$test = function($input) use ($filter) {
|
||||
return $filter->process($input, 'und', FALSE, '');
|
||||
};
|
||||
|
||||
// No data-caption nor data-align attributes.
|
||||
$input = '<img src="llama.jpg" />';
|
||||
$expected = $input;
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Only data-caption attribute.
|
||||
$input = '<img src="llama.jpg" data-caption="Loquacious llama!" />';
|
||||
$expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Empty data-caption attribute.
|
||||
$input = '<img src="llama.jpg" data-caption="" />';
|
||||
$expected = '<img src="llama.jpg" />';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// HTML entities in the caption.
|
||||
$input = '<img src="llama.jpg" data-caption="“Loquacious llama!”" />';
|
||||
$expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>“Loquacious llama!”</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// HTML encoded as HTML entities in data-caption attribute.
|
||||
$input = '<img src="llama.jpg" data-caption="<em>Loquacious llama!</em>" />';
|
||||
$expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption><em>Loquacious llama!</em></figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// HTML (not encoded as HTML entities) in data-caption attribute, which is
|
||||
// not allowed by the HTML spec, but may happen when people manually write
|
||||
// HTML, so we explicitly support it.
|
||||
$input = '<img src="llama.jpg" data-caption="<em>Loquacious llama!</em>" />';
|
||||
$expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption><em>Loquacious llama!</em></figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Security test: attempt an XSS.
|
||||
$input = '<img src="llama.jpg" data-caption="<script>alert(\'Loquacious llama!\')</script>" />';
|
||||
$expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>alert(\'Loquacious llama!\')</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Only data-align attribute: all 3 allowed values.
|
||||
$input = '<img src="llama.jpg" data-align="left" />';
|
||||
$expected = '<figure class="caption caption-img caption-left"><img src="llama.jpg" /></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
$input = '<img src="llama.jpg" data-align="center" />';
|
||||
$expected = '<figure class="caption caption-img caption-center"><img src="llama.jpg" /></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
$input = '<img src="llama.jpg" data-align="right" />';
|
||||
$expected = '<figure class="caption caption-img caption-right"><img src="llama.jpg" /></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Only data-align attribute: a disallowed value.
|
||||
$input = '<img src="llama.jpg" data-align="left foobar" />';
|
||||
$expected = '<img src="llama.jpg" />';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Empty data-align attribute.
|
||||
$input = '<img src="llama.jpg" data-align="" />';
|
||||
$expected = '<img src="llama.jpg" />';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Both data-caption and data-align attributes.
|
||||
$input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="right" />';
|
||||
$expected = '<figure class="caption caption-img caption-right"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Both data-caption and data-align attributes, but a disallowed data-align
|
||||
// attribute value.
|
||||
$input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="left foobar" />';
|
||||
$expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Ensure the filter also works with uncommon yet valid attribute quoting.
|
||||
$input = '<img src=llama.jpg data-caption=\'Loquacious llama!\' data-align=right />';
|
||||
$expected = '<figure class="caption caption-img caption-right"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Security test: attempt to inject an additional class.
|
||||
$input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center another-class-here" />';
|
||||
$expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Security test: attempt an XSS.
|
||||
$input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center \'onclick=\'alert(foo);" />';
|
||||
$expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
|
||||
// Finally, ensure that this also works on any other tag.
|
||||
$input = '<video src="llama.jpg" data-caption="Loquacious llama!" />';
|
||||
$expected = '<figure class="caption caption-video"><video src="llama.jpg"></video><figcaption>Loquacious llama!</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
$input = '<foobar data-caption="Loquacious llama!">baz</foobar>';
|
||||
$expected = '<figure class="caption caption-foobar"><foobar>baz</foobar><figcaption>Loquacious llama!</figcaption></figure>';
|
||||
$this->assertIdentical($expected, $test($input));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the line break filter.
|
||||
*/
|
||||
function testLineBreakFilter() {
|
||||
// Setup dummy filter object.
|
||||
$filter = new stdClass();
|
||||
$filter->callback = '_filter_autop';
|
||||
// Get FilterAutoP object.
|
||||
$filter = $this->filters['filter_autop'];
|
||||
|
||||
// Since the line break filter naturally needs plenty of newlines in test
|
||||
// strings and expectations, we're using "\n" instead of regular newlines
|
||||
|
@ -128,13 +236,15 @@ class FilterUnitTest extends DrupalUnitTestBase {
|
|||
* or better a whitelist approach should be used for that too.
|
||||
*/
|
||||
function testHtmlFilter() {
|
||||
// Setup dummy filter object.
|
||||
$filter = new stdClass();
|
||||
$filter->settings = array(
|
||||
'allowed_html' => '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>',
|
||||
'filter_html_help' => 1,
|
||||
'filter_html_nofollow' => 0,
|
||||
);
|
||||
// Get FilterHtml object.
|
||||
$filter = $this->filters['filter_html'];
|
||||
$filter->setPluginConfiguration(array(
|
||||
'settings' => array(
|
||||
'allowed_html' => '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>',
|
||||
'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 @@ class FilterUnitTest extends DrupalUnitTestBase {
|
|||
* Tests the spam deterrent.
|
||||
*/
|
||||
function testNoFollowFilter() {
|
||||
// Setup dummy filter object.
|
||||
$filter = new stdClass();
|
||||
$filter->settings = array(
|
||||
'allowed_html' => '<a>',
|
||||
'filter_html_help' => 1,
|
||||
'filter_html_nofollow' => 1,
|
||||
);
|
||||
// Get FilterHtml object.
|
||||
$filter = $this->filters['filter_html'];
|
||||
$filter->setPluginConfiguration(array(
|
||||
'settings' => array(
|
||||
'allowed_html' => '<a>',
|
||||
'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 @@ class FilterUnitTest extends DrupalUnitTestBase {
|
|||
* 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. <!-- \"comment\" --> Two'.\n<p>Three.</p>\n " => array(
|
||||
|
@ -224,12 +335,14 @@ class FilterUnitTest extends DrupalUnitTestBase {
|
|||
* 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 @@ www.example.com with a newline in comments -->
|
|||
$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(
|
||||
'<a href="http://www.trimmed.com/d/ff.ext?a=1&b=2#a1">www.trimmed.com/d/ff...</a>' => TRUE,
|
||||
|
@ -528,7 +645,7 @@ www.example.com with a newline in comments -->
|
|||
/**
|
||||
* 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 @@ www.example.com with a newline in comments -->
|
|||
*/
|
||||
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 @@ www.example.com with a newline in comments -->
|
|||
* - 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');
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
#}
|
||||
<figure class="caption caption-{{ tag }} {%- if align %} caption-{{ align }} {%- endif %}">
|
||||
{{ node }}
|
||||
{% if caption %}
|
||||
<figcaption>{{ caption }}</figcaption>
|
||||
{% endif %}
|
||||
</figure>
|
|
@ -14,6 +14,11 @@ filters:
|
|||
allowed_html: '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h4> <h5> <h6> <p> <span> <img>'
|
||||
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'
|
||||
|
|
|
@ -6,6 +6,11 @@ roles:
|
|||
- administrator
|
||||
cache: '1'
|
||||
filters:
|
||||
filter_caption:
|
||||
module: filter
|
||||
status: '1'
|
||||
weight: '9'
|
||||
settings: { }
|
||||
filter_htmlcorrector:
|
||||
module: filter
|
||||
status: '1'
|
||||
|
|
|
@ -1611,6 +1611,37 @@ 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;
|
||||
}
|
||||
[dir=rtl] .caption-blockquote > figcaption {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* -------------- Shortcut Links -------------- */
|
||||
|
||||
.shortcut-wrapper {
|
||||
|
|
Loading…
Reference in New Issue