mirror of
https://github.com/phpbb/phpbb.git
synced 2025-04-01 14:33:00 +02:00
Merge pull request #5545 from mrgoldy/ticket/15538
[ticket/15538] Twig icon function
This commit is contained in:
commit
8ee56d73d8
@ -29,6 +29,7 @@ imports:
|
||||
- { resource: services_text_formatter.yml }
|
||||
- { resource: services_text_reparser.yml }
|
||||
- { resource: services_twig.yml }
|
||||
- { resource: services_twig_extensions.yml }
|
||||
- { resource: services_ucp.yml }
|
||||
- { resource: services_user.yml }
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
# Twig extensions not needed by the installer
|
||||
|
||||
services:
|
||||
template.twig.extensions.icon:
|
||||
class: phpbb\template\twig\extension\icon
|
||||
arguments:
|
||||
- '@user'
|
||||
tags:
|
||||
- { name: twig.extension }
|
321
phpBB/phpbb/template/twig/extension/icon.php
Normal file
321
phpBB/phpbb/template/twig/extension/icon.php
Normal file
@ -0,0 +1,321 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* This file is part of the phpBB Forum Software package.
|
||||
*
|
||||
* @copyright (c) phpBB Limited <https://www.phpbb.com>
|
||||
* @license GNU General Public License, version 2 (GPL-2.0)
|
||||
*
|
||||
* For full copyright and license information, please see
|
||||
* the docs/CREDITS.txt file.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace phpbb\template\twig\extension;
|
||||
|
||||
use phpbb\template\twig\environment;
|
||||
|
||||
class icon extends \Twig\Extension\AbstractExtension
|
||||
{
|
||||
/** @var \phpbb\user */
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \phpbb\user $user User object
|
||||
*/
|
||||
public function __construct(\phpbb\user $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this extension.
|
||||
*
|
||||
* @return string The extension name
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'icon';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of functions to add to the existing list.
|
||||
*
|
||||
* @return \Twig\TwigFunction[] Array of twig functions
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new \Twig\TwigFunction('Icon', [$this, 'icon'], ['needs_environment' => true]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate icon HTML for use in the template, depending on the mode.
|
||||
*
|
||||
* @param environment $environment Twig environment object
|
||||
* @param string $type Icon type (font|iconify|png|svg)
|
||||
* @param string $icon Icon name (eg. "bold")
|
||||
* @param string $title Icon title
|
||||
* @param bool $hidden Hide the icon title from view
|
||||
* @param string $classes Additional classes (eg. "fa-fw")
|
||||
* @param array $attributes Additional attributes for the icon, where the key is the attribute.
|
||||
* {'data-ajax': 'mark_forums'} results in ' data-ajax="mark_forums"'
|
||||
* @return string
|
||||
*/
|
||||
public function icon(environment $environment, $type, $icon, $title = '', $hidden = false, $classes = '', array $attributes = [])
|
||||
{
|
||||
$type = strtolower($type);
|
||||
$icon = is_array($icon) ? $this->get_first_icon($icon) : $icon;
|
||||
|
||||
if (empty($icon))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$not_found = false;
|
||||
$source = '';
|
||||
$view_box = '';
|
||||
|
||||
switch ($type)
|
||||
{
|
||||
case 'font':
|
||||
// Nothing to do here..
|
||||
break;
|
||||
|
||||
case 'iconify':
|
||||
$source = explode(':', $icon);
|
||||
$source = $source[0];
|
||||
break;
|
||||
|
||||
case 'png':
|
||||
$filesystem = $environment->get_filesystem();
|
||||
$root_path = $environment->get_web_root_path();
|
||||
|
||||
$board_url = defined('PHPBB_USE_BOARD_URL_PATH') && PHPBB_USE_BOARD_URL_PATH;
|
||||
$base_path = $board_url ? generate_board_url() . '/' : $root_path;
|
||||
|
||||
// Iterate over the user's styles and check for icon existance
|
||||
foreach ($this->get_style_list() as $style_path)
|
||||
{
|
||||
if ($filesystem->exists("{$root_path}styles/{$style_path}/theme/png/{$icon}.png"))
|
||||
{
|
||||
$source = "{$base_path}styles/{$style_path}/theme/png/{$icon}.png";
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the icon was found or not
|
||||
$not_found = empty($source);
|
||||
break;
|
||||
|
||||
case 'svg':
|
||||
try
|
||||
{
|
||||
// Try to load and prepare the SVG icon
|
||||
$file = $environment->load('svg/' . $icon . '.svg');
|
||||
$source = $this->prepare_svg($file, $view_box);
|
||||
|
||||
if (empty($view_box))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
catch (\Twig\Error\LoaderError $e)
|
||||
{
|
||||
// Icon was not found
|
||||
$not_found = true;
|
||||
}
|
||||
catch (\Twig\Error\Error $e)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return '';
|
||||
break;
|
||||
}
|
||||
|
||||
// If no PNG or SVG icon was found, display a default 404 SVG icon.
|
||||
if ($not_found)
|
||||
{
|
||||
try
|
||||
{
|
||||
$file = $environment->load('svg/404.svg');
|
||||
$source = $this->prepare_svg($file, $view_box);
|
||||
}
|
||||
catch (\Twig\Error\Error $e)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$type = 'svg';
|
||||
$icon = '404';
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return $environment->render("macros/icons/{$type}.twig", [
|
||||
'ATTRIBUTES' => (string) $this->implode_attributes($attributes),
|
||||
'CLASSES' => (string) $classes,
|
||||
'ICON' => (string) $icon,
|
||||
'SOURCE' => (string) $source,
|
||||
'TITLE' => (string) $title,
|
||||
'VIEW_BOX' => (string) $view_box,
|
||||
'S_HIDDEN' => (bool) $hidden,
|
||||
]);
|
||||
}
|
||||
catch (\Twig\Error\Error $e)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an SVG for usage in the template icon.
|
||||
*
|
||||
* This removes any <?xml ?> and <!DOCTYPE> elements,
|
||||
* aswell as the root <svg> and any <title> elements.
|
||||
*
|
||||
* @param \Twig\TemplateWrapper $file The SVG file loaded from the environment
|
||||
* @param string $view_box The viewBox attribute value
|
||||
* @return string The cleaned SVG
|
||||
*/
|
||||
protected function prepare_svg(\Twig\TemplateWrapper $file, &$view_box = '')
|
||||
{
|
||||
$code = $file->render();
|
||||
$code = preg_replace( "/<\?xml.+?\?>/", '', $code);
|
||||
|
||||
$doc = new \DOMDocument();
|
||||
$doc->preserveWhiteSpace = false;
|
||||
|
||||
/**
|
||||
* Suppression is needed as DOMDocument does not like HTML5 and SVGs.
|
||||
* Options parameter prevents $dom->saveHTML() from adding an <html> element.
|
||||
*/
|
||||
@$doc->loadHTML($code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
|
||||
// Remove any DOCTYPE
|
||||
foreach ($doc->childNodes as $child)
|
||||
{
|
||||
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE)
|
||||
{
|
||||
$child->parentNode->removeChild($child);
|
||||
}
|
||||
}
|
||||
|
||||
$xpath = new \DOMXPath($doc);
|
||||
|
||||
/**
|
||||
* Remove the root <svg> element
|
||||
* and all <title> elements.
|
||||
*
|
||||
* @var \DOMElement $element
|
||||
*/
|
||||
foreach ($xpath->query('/svg | //title') as $element)
|
||||
{
|
||||
if ($element->nodeName === 'svg')
|
||||
{
|
||||
// Return the viewBox attribute value of the root SVG element by reference
|
||||
$view_box = $element->getAttribute('viewbox');
|
||||
|
||||
$width = $element->getAttribute('width');
|
||||
$height = $element->getAttribute('height');
|
||||
|
||||
if (empty($view_box) && $width && $height)
|
||||
{
|
||||
$view_box = "0 0 {$width} {$height}";
|
||||
}
|
||||
|
||||
while (isset($element->firstChild))
|
||||
{
|
||||
$element->parentNode->insertBefore($element->firstChild, $element);
|
||||
}
|
||||
}
|
||||
|
||||
$element->parentNode->removeChild($element);
|
||||
}
|
||||
|
||||
$string = $doc->saveHTML();
|
||||
$string = preg_replace('/\s+/', ' ', $string);
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first icon that has a "true" value and returns it.
|
||||
*
|
||||
* This allows sending an array to the Icon() function,
|
||||
* where the keys are the icon names and the values are their checks.
|
||||
*
|
||||
* {{ Icon('font', {
|
||||
* 'bullhorn': topicrow.S_POST_GLOBAL or topicrow.S_POST_ANNOUNCE,
|
||||
* 'star': topicrow.S_POST_STICKY,
|
||||
* 'lock': topicrow.S_TOPIC_LOCKED,
|
||||
* 'fire': topicrow.S_TOPIC_HOT,
|
||||
* 'file': true,
|
||||
* }, 'MY_TITLE', true) }}
|
||||
*
|
||||
* @param array $icons Array of icons and their booleans
|
||||
* @return string The first 'true' icon
|
||||
*/
|
||||
protected function get_first_icon(array $icons)
|
||||
{
|
||||
foreach ($icons as $icon => $boolean)
|
||||
{
|
||||
// In case the key is not a string,
|
||||
// this icon does not have a check
|
||||
// so instantly return it
|
||||
if (!is_string($icon))
|
||||
{
|
||||
return $boolean;
|
||||
}
|
||||
|
||||
if ($boolean)
|
||||
{
|
||||
return $icon;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implode an associated array of attributes to a string for usage in a template.
|
||||
*
|
||||
* @param array $attributes Associated array of attributes
|
||||
* @return string
|
||||
*/
|
||||
protected function implode_attributes(array $attributes)
|
||||
{
|
||||
$string = '';
|
||||
|
||||
foreach ($attributes as $key => $value)
|
||||
{
|
||||
$string .= ' ' . $key . '="' . $value . '"';
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the style tree of the style preferred by the current user.
|
||||
*
|
||||
* @return array Style tree, most specific first
|
||||
*/
|
||||
protected function get_style_list()
|
||||
{
|
||||
$style_list = [$this->user->style['style_path']];
|
||||
|
||||
if ($this->user->style['style_parent_id'])
|
||||
{
|
||||
$style_list = array_merge($style_list, array_reverse(explode('/', $this->user->style['style_parent_tree'])));
|
||||
}
|
||||
|
||||
return $style_list;
|
||||
}
|
||||
}
|
7
phpBB/styles/all/imgs/svg/404.svg
Normal file
7
phpBB/styles/all/imgs/svg/404.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#D8D8D8" d="M0 0h512v512H0z"/>
|
||||
<path fill="#979797" fill-rule="nonzero" d="M8 6.586l496 496v2.828L8 9.414z"/>
|
||||
<path fill="#979797" fill-rule="nonzero" d="M504 7.586v2.828l-496 496v-2.828z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 330 B |
4
phpBB/styles/all/template/macros/icons/font.twig
Normal file
4
phpBB/styles/all/template/macros/icons/font.twig
Normal file
@ -0,0 +1,4 @@
|
||||
{% spaceless %}
|
||||
<i class="o-icon o-icon-font fa-{{ ICON ~ (CLASSES ? ' ' ~ CLASSES) }}"{% if S_HIDDEN %}{% if TITLE %} title="{{ lang(TITLE) }}"{% endif %} aria-hidden="true"{% endif %}{{ ATTRIBUTES }}></i>
|
||||
{% if TITLE %}<span{% if S_HIDDEN %} class="sr-only"{% endif %}>{{ lang(TITLE) }}</span>{% endif %}
|
||||
{% endspaceless %}
|
4
phpBB/styles/all/template/macros/icons/iconify.twig
Normal file
4
phpBB/styles/all/template/macros/icons/iconify.twig
Normal file
@ -0,0 +1,4 @@
|
||||
{% spaceless %}
|
||||
<i class="iconify o-icon-src-{{ SOURCE }} o-icon{{ CLASSES ? ' ' ~ CLASSES }}"{% if S_HIDDEN %}{% if TITLE %} title="{{ lang(TITLE) }}"{% endif %} aria-hidden="true"{% endif %} data-icon="{{ ICON }}" data-inline="true"{{ ATTRIBUTES }}></i>
|
||||
{% if TITLE %}<span{% if S_HIDDEN %} class="sr-only"{% endif %}>{{ lang(TITLE) }}</span>{% endif %}
|
||||
{% endspaceless %}
|
3
phpBB/styles/all/template/macros/icons/png.twig
Normal file
3
phpBB/styles/all/template/macros/icons/png.twig
Normal file
@ -0,0 +1,3 @@
|
||||
{% spaceless %}
|
||||
<img class="o-icon o-icon-png png-{{ ICON ~ (CLASSES ? ' ' ~ CLASSES) }}" src="{{ SOURCE }}" alt="{{ lang(TITLE) }}"{{ ATTRIBUTES }} />
|
||||
{% endspaceless %}
|
9
phpBB/styles/all/template/macros/icons/svg.twig
Normal file
9
phpBB/styles/all/template/macros/icons/svg.twig
Normal file
@ -0,0 +1,9 @@
|
||||
{% spaceless %}
|
||||
{% set TITLE_ID = TITLE ? TITLE|lower|replace({' ': '_'}) ~ '-' ~ random() %}
|
||||
|
||||
<svg class="o-icon o-icon-svg svg-{{ ICON ~ (CLASSES ? ' ' ~ CLASSES) }}" xmlns="http://www.w3.org/2000/svg" viewBox="{{ VIEW_BOX }}"{% if TITLE %}{% if S_HIDDEN %} aria-hidden="true"{% endif %} aria-labelledby="{{ TITLE_ID }}"{% endif %} role="img"{{ ATTRIBUTES }}>
|
||||
{% if TITLE %}<title id="{{ TITLE_ID }}">{{ lang(TITLE) }}</title>{% endif %}
|
||||
|
||||
{{ SOURCE }}
|
||||
</svg>
|
||||
{% endspaceless %}
|
File diff suppressed because one or more lines are too long
1
tests/template/templates/extension_icon_test.html
Normal file
1
tests/template/templates/extension_icon_test.html
Normal file
@ -0,0 +1 @@
|
||||
{{ Icon(type, icon, title, hidden, classes, attributes) }}
|
6
tests/template/templates/svg/dirty.svg
Normal file
6
tests/template/templates/svg/dirty.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.1 KiB |
1
tests/template/templates/svg/pencil.svg
Normal file
1
tests/template/templates/svg/pencil.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>My fake title!</title><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
After Width: | Height: | Size: 317 B |
1
tests/template/templates/svg/phone.svg
Normal file
1
tests/template/templates/svg/phone.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56-.35-.12-.74-.03-1.01.24l-1.57 1.97c-2.83-1.35-5.48-3.9-6.89-6.83l1.95-1.66c.27-.28.35-.67.24-1.02-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 3 3 3.24 3 3.99 3 13.28 10.73 21 20.01 21c.71 0 .99-.63.99-1.18v-3.45c0-.54-.45-.99-.99-.99z"/></svg>
|
After Width: | Height: | Size: 425 B |
Loading…
x
Reference in New Issue
Block a user