Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Add a third argument to `FilesystemLoader::addPath()` and `FilesystemLoader::prependPath()` to skip checking that the directory exists
* Speed up traversing nodes with node visitors
* Speed up loading compiled templates that OPcache already holds
* Speed up the `html` escaping strategy for long strings

# 3.30.0 (2026-09-25)

Expand Down
7 changes: 7 additions & 0 deletions src/Runtime/EscaperRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

final class EscaperRuntime implements RuntimeExtensionInterface
{
private const HTML_SPECIAL_CHARS = ['&' => '&amp;', '"' => '&quot;', '\'' => '&#039;', '<' => '&lt;', '>' => '&gt;'];

/** @var array<string, callable(string, string): string> */
private $escapers = [];

Expand Down Expand Up @@ -135,6 +137,11 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu
// see https://www.php.net/htmlspecialchars

if ('UTF-8' === $charset) {
// Up to PHP 8.6, htmlspecialchars() decodes every character (see https://github.com/php/php-src/pull/23957): on long valid UTF-8 strings, replacing these characters with strtr() is faster
if (\PHP_VERSION_ID < 80700 && \strlen($string) > 32 && preg_match('//u', $string)) {
return strtr($string, self::HTML_SPECIAL_CHARS);
}

return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
}

Expand Down
19 changes: 19 additions & 0 deletions tests/Runtime/EscaperRuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,25 @@ public function testHtmlEscapingConvertsSpecialChars(): void
}
}

/**
* @dataProvider provideLongStrings
*/
#[DataProvider('provideLongStrings')]
public function testHtmlEscapingLongStringsLikeHtmlspecialchars(string $string): void
{
$this->assertSame(htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'), (new EscaperRuntime())->escape($string, 'html'));
}

public static function provideLongStrings(): iterable
{
$padding = str_repeat('padding ', 5);

yield 'nothing to escape' => [$padding];
yield 'special chars' => [$padding.'<a href="#">Tom & Jerry\'s</a>'];
yield 'multibyte chars' => [$padding.'éàü 😀'];
yield 'invalid UTF-8' => [$padding."\xC3\x28 \xFF \xED\xA0\x80 &"];
}

public function testHtmlAttributeEscapingConvertsSpecialChars(): void
{
foreach ($this->htmlAttrSpecialChars as $key => $value) {
Expand Down
Loading