Created
December 22, 2022 12:33
-
-
Save vkemeter/faf367a3a6230dd6b06a41604ed5f4c0 to your computer and use it in GitHub Desktop.
A TYPO3 ViewHelper to render SVG images inline
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Supseven\Theme\ViewHelpers\Render; | |
use TYPO3\CMS\Core\Core\Environment; | |
use TYPO3\CMS\Core\Exception; | |
use TYPO3\CMS\Core\Package\PackageManager; | |
use TYPO3\CMS\Core\Utility\GeneralUtility; | |
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; | |
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; | |
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic; | |
/** | |
* Class InlineSvgViewHelper | |
* | |
* Return the adjusted content of a svg file | |
* | |
* = Examples | |
* <code title="basic inline svg"> | |
* <theme:render.inlineSvg source="{f:uri.resource(path: 'Icons/FileIcons/{file.extension}.svg', extensionName: 'theme')}" /> | |
* </code> | |
* <output> | |
* <svg><contentOfTheSvgFile</svg> | |
* <output> | |
* | |
* = Notes | |
* | |
* This ViewHelper renders Inline Svg from a given SVG File. To make the SVG accesssible, you can add an ID and a TITLE to the | |
* produced inline svg. the original title will alway be removed. if you set a id but no title, the viewhelper throws an exception | |
* and will not be rendered. | |
*/ | |
class InlineSvgViewHelper extends AbstractViewHelper | |
{ | |
use CompileWithRenderStatic; | |
/** | |
* @var bool | |
*/ | |
protected $escapeOutput = false; | |
/** | |
* Arguments initialization | |
*/ | |
public function initializeArguments(): void | |
{ | |
parent::initializeArguments(); | |
$this->registerArgument('source', 'string', 'Source of svg resource', true); | |
$this->registerArgument('class', 'string', 'Specifies an alternate class for the svg', false); | |
$this->registerArgument('width', 'string', 'Specifies a width for the svg', false); | |
$this->registerArgument('height', 'string', 'Specifies a height for the svg', false); | |
$this->registerArgument('aria-hidden', 'bool', 'Activate aria-hidden=true attribute', false); | |
$this->registerArgument('insert-style', 'string', 'Add CSS Styles', false); | |
$this->registerArgument('id', 'string', 'Add UNIQUE Id', false); | |
$this->registerArgument('title', 'string', 'Add descriptive title for the SVG', false); | |
$this->registerArgument('fill', 'string', 'add a fill color', false); | |
} | |
/** | |
* Output different objects | |
* | |
* @param array $arguments | |
* @param \Closure $renderChildrenClosure | |
* @param RenderingContextInterface $renderingContext | |
* @return string | |
*/ | |
public static function renderStatic( | |
array $arguments, | |
\Closure $renderChildrenClosure, | |
RenderingContextInterface $renderingContext | |
) { | |
if (stripos($arguments['source'], 'EXT:') !== false) { | |
$file = GeneralUtility::makeInstance(PackageManager::class)->resolvePackagePath($arguments['source']); | |
} else { | |
$file = Environment::getPublicPath() . '/' . $arguments['source']; | |
} | |
// return html comment, if file couldn't be found | |
if (empty($arguments['source']) || !file_exists($file)) { | |
return '<!-- SVG file couldn\'t be found -->'; | |
} | |
try { | |
return self::getInlineSvg($file, $arguments); | |
} catch (\Exception $e) { | |
if ($e->getCode() === 1614863553) { | |
return '<!-- ' . $e->getMessage() . ' -->'; | |
} | |
return '<!-- SVG generation produced error! -->'; | |
} | |
} | |
/** | |
* @param string $source | |
* @param array $arguments | |
* @return string | |
*/ | |
protected static function getInlineSvg(string $source, array $arguments) | |
{ | |
$svgContent = file_get_contents($source); | |
$svgContent = preg_replace('/<script[\\s\\S]*?>[\\s\\S]*?<\\/script>/i', '', $svgContent); | |
$svgContent = preg_replace('/<title[\\s\\S]*?>[\\s\\S]*?<\\/title>/i', '', $svgContent); | |
// Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept | |
$previousValueOfEntityLoader = libxml_disable_entity_loader(true); | |
$svgElement = simplexml_load_string($svgContent); | |
libxml_disable_entity_loader($previousValueOfEntityLoader); | |
// remove xml version tag | |
$domXml = dom_import_simplexml($svgElement); | |
// add role attribute for a11y | |
$domXml->setAttribute('role', 'img'); | |
// if a title should be used, create an extra node | |
if (isset($arguments['title'])) { | |
$title = $domXml->ownerDocument->createElement('title', $arguments['title']); | |
} | |
// change all fill attributes to the value given from the viewhelper | |
if ($arguments['fill']) { | |
$domXml->setAttribute('fill', $arguments['fill']); | |
foreach ($domXml->childNodes as $node) { | |
if ($node instanceof \DOMElement) { | |
$node->setAttribute('fill', $arguments['fill']); | |
foreach ($node->childNodes as $n) { | |
if ($n instanceof \DOMElement) { | |
$n->setAttribute('fill', $arguments['fill']); | |
} | |
} | |
} | |
} | |
} | |
// if there is a id, it means, this svg should be accessible. therefore a title MUST be set | |
// if not, it throws a exception and will not be rendered. | |
// if no id is set, the SVG is automatically marked as aria-hidden for a11y reasons | |
if (isset($arguments['id'])) { | |
$domXml->setAttribute('aria-labelledby', self::sanitizeId($arguments['id'])); | |
if (isset($title)) { | |
$title->setAttribute('id', self::sanitizeId($arguments['id'])); | |
} else { | |
throw new Exception('This SVG is not accessible, if there is no Title attribute available', 1614863553); | |
} | |
} else { | |
$domXml->setAttribute('aria-hidden', 'true'); | |
} | |
// at this point, the title will be appended to the svg object | |
if (isset($title)) { | |
$domXml->appendChild($title); | |
} | |
// if you have inline styles, like css variables, you can use this argument to add styles directly to the | |
// svg. | |
if (isset($arguments['insert-style'])) { | |
$inlineStyle = $domXml->ownerDocument->createElement('style', $arguments['insert-style']); | |
$domXml->appendChild($inlineStyle); | |
} | |
if (isset($arguments['class'])) { | |
$domXml->setAttribute('class', $arguments['class']); | |
} | |
if (isset($arguments['width'])) { | |
$domXml->setAttribute('width', (string)$arguments['width']); | |
} | |
if (isset($arguments['height'])) { | |
$domXml->setAttribute('height', (string)$arguments['height']); | |
} | |
return $domXml->ownerDocument->saveXML($domXml->ownerDocument->documentElement); | |
} | |
/** | |
* sanitizes a possible id string to a correct usable id string | |
* | |
* @param string $id | |
* @return string | |
*/ | |
protected static function sanitizeId(string $id): string | |
{ | |
$svgId = (string)preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $id); | |
return htmlspecialchars((string)preg_replace('/^[^a-zA-Z]/', 'x', $svgId)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment