mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-30 03:57:21 +00:00
f640320202
* 3.x: (26 commits) Remove the documentation comments compilation overhead Clarify source function trust requirements Throw on PCRE errors in the matches operator Document that reusing a non-rewindable iterator after destructuring is unsupported Release destructuring temporaries after assignment Deprecate prefixed macro definedness checks Fix duplicate macro deprecation wording Throw when list formatting fails Document that sequence destructuring consumes one value per pattern slot Fix the html_attr documentation about iterables in data attributes Warn about untrusted input with the default Tempest markdown converter Document that overriding MacroNode::compile() is not supported anymore Merge overlapping CHANGELOG entries for the destructuring fatal error fix Document that include_only keeps global variables available Remove lazy macro import resolution Honor date formatter prototype calendars Fix Stringable keys for ArrayAccess implementations Fix repeated object destructuring evaluation Restore void return type compatibility for extension points Reject destructuring patterns containing no variables ... # Conflicts: # CHANGELOG # doc/deprecated.rst # doc/filters/format_datetime.rst # extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php # extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php # extra/twig-extra-bundle/TwigExtraBundle.php # src/MacroNamespace.php # src/Node/MacrosNode.php # src/Parser.php # src/Test/IntegrationTestCase.php # src/Test/NodeTestCase.php # tests/CallMacroTest.php # tests/ExpressionParserTest.php # tests/Fixtures/macros/duplicate_definition.legacy.test # tests/Node/MacrosTest.php # tests/ParserTest.php
289 lines
10 KiB
PHP
289 lines
10 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of Twig.
|
|
*
|
|
* (c) Fabien Potencier
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Twig\Extra\Html;
|
|
|
|
use Symfony\Component\Mime\MimeTypes;
|
|
use Twig\Environment;
|
|
use Twig\Error\RuntimeError;
|
|
use Twig\Extension\AbstractExtension;
|
|
use Twig\Extra\Html\HtmlAttr\AttributeValueInterface;
|
|
use Twig\Extra\Html\HtmlAttr\InlineStyle;
|
|
use Twig\Extra\Html\HtmlAttr\MergeableInterface;
|
|
use Twig\Extra\Html\HtmlAttr\SeparatedTokenList;
|
|
use Twig\Markup;
|
|
use Twig\Runtime\EscaperRuntime;
|
|
use Twig\TwigFilter;
|
|
use Twig\TwigFunction;
|
|
|
|
final class HtmlExtension extends AbstractExtension
|
|
{
|
|
private $mimeTypes;
|
|
|
|
public function __construct(?MimeTypes $mimeTypes = null)
|
|
{
|
|
$this->mimeTypes = $mimeTypes;
|
|
}
|
|
|
|
public function getFilters(): array
|
|
{
|
|
return [
|
|
new TwigFilter('data_uri', [$this, 'dataUri']),
|
|
new TwigFilter('html_attr_merge', [self::class, 'htmlAttrMerge']),
|
|
new TwigFilter('html_attr_type', [self::class, 'htmlAttrType']),
|
|
];
|
|
}
|
|
|
|
public function getFunctions(): array
|
|
{
|
|
return [
|
|
new TwigFunction('html_classes', self::htmlClasses(...)),
|
|
new TwigFunction('html_cva', self::htmlCva(...)),
|
|
new TwigFunction('html_attr', self::htmlAttr(...), ['needs_environment' => true, 'is_safe' => ['html']]),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Creates a data URI (RFC 2397).
|
|
*
|
|
* Length validation is not performed on purpose, validation should
|
|
* be done before calling this filter.
|
|
*
|
|
* @return string The generated data URI
|
|
*
|
|
* @internal
|
|
*/
|
|
public function dataUri(string $data, ?string $mime = null, array $parameters = []): string
|
|
{
|
|
$repr = 'data:';
|
|
|
|
if (null === $mime) {
|
|
if (null === $this->mimeTypes) {
|
|
$this->mimeTypes = new MimeTypes();
|
|
}
|
|
|
|
$tmp = tempnam(sys_get_temp_dir(), 'mime');
|
|
file_put_contents($tmp, $data);
|
|
try {
|
|
if (null === $mime = $this->mimeTypes->guessMimeType($tmp)) {
|
|
$mime = 'text/plain';
|
|
}
|
|
} finally {
|
|
@unlink($tmp);
|
|
}
|
|
}
|
|
$repr .= $mime;
|
|
|
|
foreach ($parameters as $key => $value) {
|
|
$repr .= ';'.$key.'='.rawurlencode($value);
|
|
}
|
|
|
|
if (str_starts_with($mime, 'text/')) {
|
|
$repr .= ','.rawurlencode($data);
|
|
} else {
|
|
$repr .= ';base64,'.base64_encode($data);
|
|
}
|
|
|
|
return $repr;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
public static function htmlClasses(...$args): string
|
|
{
|
|
$classes = [];
|
|
foreach ($args as $i => $arg) {
|
|
if (\is_string($arg) || $arg instanceof Markup) {
|
|
$classes[] = (string) $arg;
|
|
} elseif (\is_array($arg)) {
|
|
foreach ($arg as $class => $condition) {
|
|
if (!\is_string($class)) {
|
|
throw new RuntimeError(\sprintf('The "html_classes" function argument %d (key %d) should be a string, got "%s".', $i, $class, get_debug_type($class)));
|
|
}
|
|
if (!$condition) {
|
|
continue;
|
|
}
|
|
$classes[] = $class;
|
|
}
|
|
} else {
|
|
throw new RuntimeError(\sprintf('The "html_classes" function argument %d should be either a string or an array, got "%s".', $i, get_debug_type($arg)));
|
|
}
|
|
}
|
|
|
|
return implode(' ', array_unique(array_filter($classes, static function ($v) { return '' !== $v; })));
|
|
}
|
|
|
|
/**
|
|
* @param string|list<string|null> $base
|
|
* @param array<string, array<string, string|array<string>>> $variants
|
|
* @param array<array<string, string|array<string>>> $compoundVariants
|
|
* @param array<string, string> $defaultVariant
|
|
*
|
|
* @internal
|
|
*/
|
|
public static function htmlCva(array|string $base = [], array $variants = [], array $compoundVariants = [], array $defaultVariant = []): Cva
|
|
{
|
|
return new Cva($base, $variants, $compoundVariants, $defaultVariant);
|
|
}
|
|
|
|
/** @internal */
|
|
public static function htmlAttrType(mixed $value, string $type = 'sst'): AttributeValueInterface
|
|
{
|
|
return match ($type) {
|
|
'sst' => new SeparatedTokenList($value, ' '),
|
|
'cst' => new SeparatedTokenList($value, ', '),
|
|
'style' => new InlineStyle($value),
|
|
default => throw new RuntimeError(\sprintf('Unknown attribute type "%s" The only supported types are "sst", "cst" and "style".', $type)),
|
|
};
|
|
}
|
|
|
|
/** @internal */
|
|
public static function htmlAttrMerge(iterable|string|false|null ...$arrays): array
|
|
{
|
|
$result = [];
|
|
|
|
foreach ($arrays as $array) {
|
|
if (!$array) {
|
|
continue;
|
|
}
|
|
|
|
if (\is_string($array)) {
|
|
throw new RuntimeError('Only empty strings may be passed as string arguments to html_attr_merge. This is to support the implicit else clause for ternary operators.');
|
|
}
|
|
|
|
foreach ($array as $key => $value) {
|
|
if (!isset($result[$key])) {
|
|
$result[$key] = $value;
|
|
|
|
continue;
|
|
}
|
|
|
|
$existing = $result[$key];
|
|
|
|
switch (true) {
|
|
case $value instanceof MergeableInterface:
|
|
$result[$key] = $value->mergeInto($existing);
|
|
break;
|
|
case $existing instanceof MergeableInterface:
|
|
$result[$key] = $existing->appendFrom($value);
|
|
break;
|
|
case is_iterable($existing) && is_iterable($value):
|
|
$result[$key] = [...$existing, ...$value];
|
|
break;
|
|
case (\is_scalar($existing) || \is_object($existing)) && (\is_scalar($value) || \is_object($value)):
|
|
$result[$key] = $value;
|
|
break;
|
|
default:
|
|
throw new RuntimeError(\sprintf('Cannot merge incompatible values for key "%s".', $key));
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/** @internal */
|
|
public static function htmlAttr(Environment $env, iterable|string|false|null ...$args): string
|
|
{
|
|
$attr = self::htmlAttrMerge(...$args);
|
|
|
|
$result = '';
|
|
$runtime = $env->getRuntime(EscaperRuntime::class);
|
|
|
|
foreach ($attr as $name => $value) {
|
|
if (null === $value = self::htmlAttrValue($name, $value)) {
|
|
continue;
|
|
}
|
|
|
|
$result .= $runtime->escape($name, 'html_attr_relaxed').'="'.$runtime->escape($value).'" ';
|
|
}
|
|
|
|
return trim($result);
|
|
}
|
|
|
|
/**
|
|
* Resolves the final value of a single HTML attribute the way the "html_attr"
|
|
* function renders it, without escaping it.
|
|
*
|
|
* The returned string is meant to be printed as the value of the given
|
|
* attribute; it MUST be escaped for the HTML attribute context before being
|
|
* printed. A null return means the attribute must be omitted (a null or false
|
|
* value, except for aria-* attributes where false becomes the "false" string).
|
|
*
|
|
* @param string $name The attribute name, which drives the aria-*, data-* and style handling
|
|
* @param mixed $value The raw attribute value
|
|
*/
|
|
public static function htmlAttrValue(string $name, mixed $value): ?string
|
|
{
|
|
if ($value instanceof \BackedEnum) {
|
|
$value = $value->value;
|
|
}
|
|
|
|
if (str_starts_with($name, 'aria-')) {
|
|
// For aria-*, convert booleans to "true" and "false" strings
|
|
if (true === $value) {
|
|
$value = 'true';
|
|
} elseif (false === $value) {
|
|
$value = 'false';
|
|
}
|
|
}
|
|
|
|
if (str_starts_with($name, 'data-')) {
|
|
if (!$value instanceof AttributeValueInterface && !$value instanceof \Stringable && null !== $value && !\is_scalar($value)) {
|
|
// ... encode non-null non-scalars as JSON, but leave the string representation
|
|
// of a Stringable alone, as it is already the value the object asks to render as
|
|
try {
|
|
$value = json_encode($value, \JSON_THROW_ON_ERROR);
|
|
} catch (\JsonException $e) {
|
|
throw new RuntimeError(\sprintf('The "%s" attribute value cannot be JSON encoded.', $name), previous: $e);
|
|
}
|
|
} elseif (true === $value) {
|
|
// ... and convert boolean true to a 'true' string.
|
|
$value = 'true';
|
|
}
|
|
}
|
|
|
|
// Convert iterable values to token lists
|
|
if (!$value instanceof AttributeValueInterface && is_iterable($value)) {
|
|
if ('style' === $name) {
|
|
$value = new InlineStyle($value);
|
|
} else {
|
|
$value = new SeparatedTokenList($value);
|
|
}
|
|
}
|
|
|
|
if ($value instanceof AttributeValueInterface) {
|
|
$value = $value->getValue();
|
|
}
|
|
|
|
// In general, ...
|
|
if (true === $value) {
|
|
// ... use attribute="" for boolean true,
|
|
// which is XHTML compliant and indicates the "empty value default", see
|
|
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 and
|
|
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes
|
|
$value = '';
|
|
}
|
|
|
|
if (null === $value || false === $value) {
|
|
// omit null-valued and false attributes completely (note aria-* has been processed before)
|
|
return null;
|
|
}
|
|
|
|
if (\is_object($value) && !$value instanceof \Stringable) {
|
|
throw new RuntimeError(\sprintf('The "%s" attribute value should be a scalar, an iterable, or an object implementing "%s", got "%s".', $name, \Stringable::class, get_debug_type($value)));
|
|
}
|
|
|
|
return (string) $value;
|
|
}
|
|
}
|