[FEATURE] Add inline render support for SvgIconProvider 77/43877/8
authorFrank Nägler <frank.naegler@typo3.org>
Tue, 6 Oct 2015 22:58:53 +0000 (00:58 +0200)
committerFrank Nägler <frank.naegler@typo3.org>
Mon, 12 Oct 2015 09:41:55 +0000 (11:41 +0200)
Resolves: #69706
Releases: master
Change-Id: Ibea120d8eb5057baed9c216a076879368982ada9
Reviewed-on: http://review.typo3.org/43877
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Benjamin Kott <info@bk2k.info>
Tested-by: Benjamin Kott <info@bk2k.info>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
typo3/sysext/core/Classes/Imaging/Icon.php
typo3/sysext/core/Classes/Imaging/IconProvider/SvgIconProvider.php
typo3/sysext/core/Classes/ViewHelpers/IconViewHelper.php
typo3/sysext/core/Documentation/Changelog/master/Feature-69706-AddInlineSupportForSvgIconProvider.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Imaging/IconProvider/SvgIconProviderTest.php
typo3/sysext/core/Tests/Unit/ViewHelpers/IconViewHelperTest.php

index e8e6465..2a2481a 100644 (file)
@@ -90,11 +90,22 @@ class Icon
     protected $markup;
 
     /**
+     * @var array
+     */
+    protected $alternativeMarkups = array();
+
+    /**
      * @internal this method is used for internal processing, to get the prepared and final markup use render()
+     *
+     * @param string $alternativeMarkupIdentifier
+     *
      * @return string
      */
-    public function getMarkup()
+    public function getMarkup($alternativeMarkupIdentifier = null)
     {
+        if ($alternativeMarkupIdentifier !== null && isset($this->alternativeMarkups[$alternativeMarkupIdentifier])) {
+            return $this->alternativeMarkups[$alternativeMarkupIdentifier];
+        }
         return $this->markup;
     }
 
@@ -107,6 +118,25 @@ class Icon
     }
 
     /**
+     * @param string $markupIdentifier
+     *
+     * @return string
+     */
+    public function getAlternativeMarkup($markupIdentifier)
+    {
+        return $this->alternativeMarkups[$markupIdentifier];
+    }
+
+    /**
+     * @param string $markupIdentifier
+     * @param string $markup
+     */
+    public function setAlternativeMarkup($markupIdentifier, $markup)
+    {
+        $this->alternativeMarkups[$markupIdentifier] = $markup;
+    }
+
+    /**
      * @return string
      */
     public function getIdentifier()
@@ -202,15 +232,17 @@ class Icon
     /**
      * Render the icon as HTML code
      *
+     * @param string $alternativeMarkupIdentifier
+     *
      * @return string
      */
-    public function render()
+    public function render($alternativeMarkupIdentifier = null)
     {
         $overlayIconMarkup = '';
         if ($this->overlayIcon !== null) {
             $overlayIconMarkup = '<span class="icon-overlay icon-' . htmlspecialchars($this->overlayIcon->getIdentifier()) . '">' . $this->overlayIcon->getMarkup() . '</span>';
         }
-        return str_replace('{overlayMarkup}', $overlayIconMarkup, $this->wrappedIcon());
+        return str_replace('{overlayMarkup}', $overlayIconMarkup, $this->wrappedIcon($alternativeMarkupIdentifier));
     }
 
     /**
@@ -226,9 +258,11 @@ class Icon
     /**
      * Wrap icon markup in unified HTML code
      *
+     * @param string $alternativeMarkupIdentifier
+     *
      * @return string
      */
-    protected function wrappedIcon()
+    protected function wrappedIcon($alternativeMarkupIdentifier = null)
     {
         $classes = array();
         $classes[] = 'icon';
@@ -242,7 +276,7 @@ class Icon
         $markup = array();
         $markup[] = '<span class="' . htmlspecialchars(implode(' ', $classes)) . '">';
         $markup[] = '  <span class="icon-markup">';
-        $markup[] = $this->getMarkup();
+        $markup[] = $this->getMarkup($alternativeMarkupIdentifier);
         $markup[] = '  </span>';
         $markup[] = '  {overlayMarkup}';
         $markup[] = '</span>';
index 6417d26..1b9ce84 100644 (file)
@@ -25,6 +25,8 @@ use TYPO3\CMS\Core\Utility\StringUtility;
  */
 class SvgIconProvider implements IconProviderInterface
 {
+    const MARKUP_IDENTIFIER_INLINE = 'inline';
+
     /**
      * @param Icon $icon
      * @param array $options
@@ -32,6 +34,7 @@ class SvgIconProvider implements IconProviderInterface
     public function prepareIconMarkup(Icon $icon, array $options = array())
     {
         $icon->setMarkup($this->generateMarkup($icon, $options));
+        $icon->setAlternativeMarkup(self::MARKUP_IDENTIFIER_INLINE, $this->generateInlineMarkup($icon, $options));
     }
 
     /**
@@ -51,8 +54,49 @@ class SvgIconProvider implements IconProviderInterface
         if (StringUtility::beginsWith($source, 'EXT:') || !StringUtility::beginsWith($source, '/')) {
             $source = GeneralUtility::getFileAbsFileName($source);
         }
-        $source = PathUtility::getAbsoluteWebPath($source);
 
+        $source = PathUtility::getAbsoluteWebPath($source);
         return '<img src="' . htmlspecialchars($source) . '" width="' . $icon->getDimension()->getWidth() . '" height="' . $icon->getDimension()->getHeight() . '" />';
     }
+
+    /**
+     * @param Icon $icon
+     * @param array $options
+     * @return string
+     * @throws \InvalidArgumentException
+     */
+    protected function generateInlineMarkup(Icon $icon, array $options)
+    {
+        if (empty($options['source'])) {
+            throw new \InvalidArgumentException('The option "source" is required and must not be empty', 1440754980);
+        }
+
+        $source = $options['source'];
+
+        if (StringUtility::beginsWith($source, 'EXT:') || !StringUtility::beginsWith($source, '/')) {
+            $source = GeneralUtility::getFileAbsFileName($source);
+        }
+
+        return $this->getInlineSvg($source);
+    }
+
+    /**
+     * @param string $source
+     *
+     * @return string
+     */
+    protected function getInlineSvg($source)
+    {
+        if (!file_exists($source)) {
+            return '';
+        }
+
+        $svgContent = file_get_contents($source);
+        $svgContent = preg_replace('/<script[\s\S]*?>[\s\S]*?<\/script>/i', '', $svgContent);
+        $svgElement = simplexml_load_string($svgContent);
+
+        // remove xml version tag
+        $domXml = dom_import_simplexml($svgElement);
+        return $domXml->ownerDocument->saveXML($domXml->ownerDocument->documentElement);
+    }
 }
index a51aa55..a0e295a 100644 (file)
@@ -34,16 +34,18 @@ class IconViewHelper extends AbstractViewHelper implements CompilableInterface
      * @param string $size
      * @param string $overlay
      * @param string $state
+     * @param string $alternativeMarkupIdentifier
      * @return string
      */
-    public function render($identifier, $size = Icon::SIZE_SMALL, $overlay = null, $state = IconState::STATE_DEFAULT)
+    public function render($identifier, $size = Icon::SIZE_SMALL, $overlay = null, $state = IconState::STATE_DEFAULT, $alternativeMarkupIdentifier = null)
     {
         return static::renderStatic(
             array(
                 'identifier' => $identifier,
                 'size' => $size,
                 'overlay' => $overlay,
-                'state' => $state
+                'state' => $state,
+                'alternativeMarkupIdentifier' => $alternativeMarkupIdentifier
             ),
             $this->buildRenderChildrenClosure(),
             $this->renderingContext
@@ -64,8 +66,9 @@ class IconViewHelper extends AbstractViewHelper implements CompilableInterface
         $size = $arguments['size'];
         $overlay = $arguments['overlay'];
         $state = IconState::cast($arguments['state']);
+        $alternativeMarkupIdentifier = $arguments['alternativeMarkupIdentifier'];
         /** @var IconFactory $iconFactory */
         $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        return $iconFactory->getIcon($identifier, $size, $overlay, $state)->render();
+        return $iconFactory->getIcon($identifier, $size, $overlay, $state)->render($alternativeMarkupIdentifier);
     }
 }
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-69706-AddInlineSupportForSvgIconProvider.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-69706-AddInlineSupportForSvgIconProvider.rst
new file mode 100644 (file)
index 0000000..d8897ce
--- /dev/null
@@ -0,0 +1,18 @@
+==================================================================
+Feature: #69706 - Add support for alternative (inline) icon markup
+==================================================================
+
+Description
+===========
+
+It is now possible to set alternative markups for an ``Icon``.
+By default icon is rendered as ``<img src="..."/>`` tag with path to the icon file in the src attribute. With this change it's possible to render svg icon inline in the html e.g. ``<svg>...</svg>``. Placing SVG images inline allows to manipulate them using CSS or JS.
+
+.. code-block:: php
+
+       $icon->setAlternativeMarkup(SvgIconProvider::MARKUP_IDENTIFIER_INLINE, '<svg>...</svg>');
+
+Impact
+======
+
+An IconProvider can now add multiple markup variants for an icon.
index 4751129..d187423 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Imaging\IconProvider;
  */
 
 use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
@@ -33,6 +34,11 @@ class SvgIconProviderTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
     protected $icon = null;
 
     /**
+     * @var string
+     */
+    protected $testFileName;
+
+    /**
      * Set up
      *
      * @return void
@@ -43,6 +49,18 @@ class SvgIconProviderTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $this->icon = GeneralUtility::makeInstance(Icon::class);
         $this->icon->setIdentifier('foo');
         $this->icon->setSize(Icon::SIZE_SMALL);
+
+        $svgTestFileContent = '<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#CD201F" d="M11 12l3-2v6H2v-6l3 2 3-2 3 2z"></path><script><![CDATA[ function alertMe() {} ]]></script></svg>';
+        $this->testFileName = GeneralUtility::tempnam(uniqid('svg_') . '.svg');
+        file_put_contents($this->testFileName, $svgTestFileContent);
+    }
+
+    /**
+     * Tear down
+     */
+    protected function tearDown()
+    {
+        unlink($this->testFileName);
     }
 
     /**
@@ -71,4 +89,13 @@ class SvgIconProviderTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $this->subject->prepareIconMarkup($this->icon, array('source' => 'EXT:core/Resources/Public/Images/foo.svg'));
         $this->assertEquals('<img src="typo3/sysext/core/Resources/Public/Images/foo.svg" width="16" height="16" />', $this->icon->getMarkup());
     }
+
+    /**
+     * @test
+     */
+    public function getIconWithInlineOptionReturnsCleanSvgMarkup()
+    {
+        $this->subject->prepareIconMarkup($this->icon, array('source' => $this->testFileName));
+        $this->assertEquals('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#CD201F" d="M11 12l3-2v6H2v-6l3 2 3-2 3 2z"/></svg>', $this->icon->getMarkup(SvgIconProvider::MARKUP_IDENTIFIER_INLINE));
+    }
 }
index 917fac9..b2809bb 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\ViewHelpers;
 use Prophecy\Argument;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
 use TYPO3\CMS\Core\Type\Icon\IconState;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\ViewHelpers\IconViewHelper;
@@ -50,7 +51,7 @@ class IconViewHelperTest extends ViewHelperBaseTestcase
         $iconProphecy = $this->prophesize(Icon::class);
 
         $iconFactoryProphecy->getIcon('myIdentifier', Icon::SIZE_SMALL, null, IconState::cast(IconState::STATE_DEFAULT))->shouldBeCalled()->willReturn($iconProphecy->reveal());
-        $iconProphecy->render()->shouldBeCalled()->willReturn('htmlFoo');
+        $iconProphecy->render(NULL)->shouldBeCalled()->willReturn('htmlFoo');
 
         $this->assertSame('htmlFoo', $this->viewHelper->render('myIdentifier'));
     }
@@ -65,7 +66,7 @@ class IconViewHelperTest extends ViewHelperBaseTestcase
         $iconProphecy = $this->prophesize(Icon::class);
 
         $iconFactoryProphecy->getIcon('myIdentifier', Icon::SIZE_LARGE, null, IconState::cast(IconState::STATE_DEFAULT))->shouldBeCalled()->willReturn($iconProphecy->reveal());
-        $iconProphecy->render()->shouldBeCalled()->willReturn('htmlFoo');
+        $iconProphecy->render(NULL)->shouldBeCalled()->willReturn('htmlFoo');
 
         $this->assertSame('htmlFoo', $this->viewHelper->render('myIdentifier', Icon::SIZE_LARGE));
     }
@@ -80,7 +81,7 @@ class IconViewHelperTest extends ViewHelperBaseTestcase
         $iconProphecy = $this->prophesize(Icon::class);
 
         $iconFactoryProphecy->getIcon('myIdentifier', Icon::SIZE_SMALL, null, IconState::cast(IconState::STATE_DISABLED))->shouldBeCalled()->willReturn($iconProphecy->reveal());
-        $iconProphecy->render()->shouldBeCalled()->willReturn('htmlFoo');
+        $iconProphecy->render(NULL)->shouldBeCalled()->willReturn('htmlFoo');
 
         $this->assertSame('htmlFoo', $this->viewHelper->render('myIdentifier', Icon::SIZE_SMALL, null, IconState::cast(IconState::STATE_DISABLED)));
     }
@@ -95,7 +96,7 @@ class IconViewHelperTest extends ViewHelperBaseTestcase
         $iconProphecy = $this->prophesize(Icon::class);
 
         $iconFactoryProphecy->getIcon('myIdentifier', Argument::any(), 'overlayString', IconState::cast(IconState::STATE_DEFAULT))->shouldBeCalled()->willReturn($iconProphecy->reveal());
-        $iconProphecy->render()->shouldBeCalled()->willReturn('htmlFoo');
+        $iconProphecy->render(NULL)->shouldBeCalled()->willReturn('htmlFoo');
 
         $this->assertSame('htmlFoo', $this->viewHelper->render('myIdentifier', Icon::SIZE_LARGE, 'overlayString'));
     }