Skip to content

Add Illuminate\Support\EncodedHtmlString #54737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1744ba5
wip
crynobone Feb 21, 2025
37a4a89
wip
crynobone Feb 21, 2025
ba5c5b0
wip
crynobone Feb 21, 2025
98b52b1
wip
crynobone Feb 21, 2025
f2317c3
wip
crynobone Feb 21, 2025
6aa1f01
wip
crynobone Feb 21, 2025
e54d8a0
wip
crynobone Feb 21, 2025
ca869ce
wip
crynobone Feb 21, 2025
f76996f
wip
crynobone Feb 21, 2025
208256f
wip
crynobone Feb 21, 2025
cdca973
wip
crynobone Feb 21, 2025
d405551
wip
crynobone Feb 21, 2025
1f0889f
wip
crynobone Feb 21, 2025
bbd61dc
wip
crynobone Feb 24, 2025
1859539
Apply fixes from StyleCI
StyleCIBot Feb 24, 2025
e1b4315
wip
crynobone Feb 24, 2025
b3a8891
Merge remote-tracking branch 'upstream/markdown-string' into markdown…
crynobone Feb 24, 2025
dc4e4ba
wip
crynobone Feb 24, 2025
4dfa33c
wip
crynobone Feb 24, 2025
93d8418
wip
crynobone Feb 24, 2025
cd647d1
Apply fixes from StyleCI
StyleCIBot Feb 24, 2025
4bf64bb
wip
crynobone Feb 24, 2025
cd5b117
Merge remote-tracking branch 'upstream/markdown-string' into markdown…
crynobone Feb 24, 2025
ab43534
wip
crynobone Feb 24, 2025
823571a
Update EncodedHtmlString.php
crynobone Feb 24, 2025
1c578fe
wip
crynobone Feb 28, 2025
8b9fe3e
wip
crynobone Feb 28, 2025
20b8503
wip
crynobone Feb 28, 2025
7eac843
wip
crynobone Feb 28, 2025
9fd2d7e
Apply fixes from StyleCI
StyleCIBot Feb 28, 2025
18cb73d
Update EncodedHtmlString.php
crynobone Feb 28, 2025
23e9df0
Update Markdown.php
crynobone Feb 28, 2025
87abdeb
Update src/Illuminate/Mail/Markdown.php
crynobone Feb 28, 2025
6138315
wip
crynobone Mar 4, 2025
a68cc34
Apply fixes from StyleCI
StyleCIBot Mar 4, 2025
969b82c
wip
crynobone Mar 4, 2025
26b21bd
Merge remote-tracking branch 'upstream/markdown-string' into markdown…
crynobone Mar 4, 2025
cfeeafb
wip
crynobone Mar 4, 2025
38db599
wip
crynobone Mar 4, 2025
35d8b50
wip
crynobone Mar 4, 2025
0e68768
wip
crynobone Mar 4, 2025
72f0075
wip
crynobone Mar 4, 2025
2b4d4b4
wip
crynobone Mar 5, 2025
72a85b3
wip
crynobone Mar 5, 2025
5b4f029
formatting
taylorotwell Mar 18, 2025
7f84895
formatting
taylorotwell Mar 18, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Illuminate\Queue\Console\WorkCommand;
use Illuminate\Queue\Queue;
use Illuminate\Support\Carbon;
use Illuminate\Support\EncodedHtmlString;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\ParallelTesting;
use Illuminate\Support\Once;
Expand Down Expand Up @@ -171,6 +172,7 @@ protected function tearDownTheTestEnvironment(): void
Component::forgetFactory();
ConvertEmptyStringsToNull::flushState();
Factory::flushState();
EncodedHtmlString::flushState();
EncryptCookies::flushState();
HandleExceptions::flushState();
Migrator::withoutMigrations([]);
Expand Down
9 changes: 6 additions & 3 deletions src/Illuminate/Mail/Mailable.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Contracts\Translation\HasLocalePreference;
use Illuminate\Support\Collection;
use Illuminate\Support\EncodedHtmlString;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
Expand Down Expand Up @@ -1371,7 +1372,7 @@ public function assertHasSubject($subject)
*/
public function assertSeeInHtml($string, $escape = true)
{
$string = $escape ? e($string) : $string;
$string = $escape ? EncodedHtmlString::convert($string, withQuote: isset($this->markdown)) : $string;

[$html, $text] = $this->renderForAssertions();

Expand All @@ -1393,7 +1394,7 @@ public function assertSeeInHtml($string, $escape = true)
*/
public function assertDontSeeInHtml($string, $escape = true)
{
$string = $escape ? e($string) : $string;
$string = $escape ? EncodedHtmlString::convert($string, withQuote: isset($this->markdown)) : $string;

[$html, $text] = $this->renderForAssertions();

Expand All @@ -1415,7 +1416,9 @@ public function assertDontSeeInHtml($string, $escape = true)
*/
public function assertSeeInOrderInHtml($strings, $escape = true)
{
$strings = $escape ? array_map('e', $strings) : $strings;
$strings = $escape ? array_map(function ($string) {
return EncodedHtmlString::convert($string, withQuote: isset($this->markdown));
}, $strings) : $strings;

[$html, $text] = $this->renderForAssertions();

Expand Down
59 changes: 51 additions & 8 deletions src/Illuminate/Mail/Markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Illuminate\Mail;

use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Support\EncodedHtmlString;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use League\CommonMark\Environment\Environment;
Expand Down Expand Up @@ -60,9 +61,19 @@ public function render($view, array $data = [], $inliner = null)
{
$this->view->flushFinderCache();

$contents = $this->view->replaceNamespace(
'mail', $this->htmlComponentPaths()
)->make($view, $data)->render();
$bladeCompiler = $this->view
->getEngineResolver()
->resolve('blade')
->getCompiler();

$contents = $bladeCompiler->usingEchoFormat(
'new \Illuminate\Support\EncodedHtmlString(%s)',
function () use ($view, $data) {
return $this->view->replaceNamespace(
'mail', $this->htmlComponentPaths()
)->make($view, $data)->render();
}
);

if ($this->view->exists($customTheme = Str::start($this->theme, 'mail.'))) {
$theme = $customTheme;
Expand Down Expand Up @@ -105,16 +116,48 @@ public function renderText($view, array $data = [])
*/
public static function parse($text)
{
$environment = new Environment([
EncodedHtmlString::encodeUsing(function ($value) {
$replacements = [
'[' => '\[',
'<' => '\<',
];

$html = str_replace(array_keys($replacements), array_values($replacements), $value);

return static::converter([
'html_input' => 'escape',
])->convert($html)->getContent();
});

$html = '';

try {
$html = static::converter()->convert($text)->getContent();
} finally {
EncodedHtmlString::flushState();
}

return new HtmlString($html);
}

/**
* Get a Markdown converter instance.
*
* @internal
*
* @param array<string, mixed> $config
* @return \League\CommonMark\MarkdownConverter
*/
public static function converter(array $config = [])
{
$environment = new Environment(array_merge([
'allow_unsafe_links' => false,
]);
], $config));

$environment->addExtension(new CommonMarkCoreExtension);
$environment->addExtension(new TableExtension);

$converter = new MarkdownConverter($environment);

return new HtmlString($converter->convert($text)->getContent());
return new MarkdownConverter($environment);
}

/**
Expand Down
76 changes: 76 additions & 0 deletions src/Illuminate/Support/EncodedHtmlString.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Illuminate\Support;

class EncodedHtmlString extends HtmlString
{
/**
* The callback that should be used to encode the HTML strings.
*
* @var callable|null
*/
protected static $encodeUsingFactory;

/**
* Create a new encoded HTML string instance.
*
* @param string $html
* @param bool $doubleEncode
* @return void
*/
public function __construct($html = '', protected bool $doubleEncode = true)
{
parent::__construct($html);
}

/**
* Convert the special characters in the given value.
*
* @internal
*
* @param string|null $value
* @param int $withQuote
* @param bool $doubleEncode
* @return string
*/
public static function convert($value, bool $withQuote = true, bool $doubleEncode = true)
{
$flag = $withQuote ? ENT_QUOTES : ENT_NOQUOTES;

return htmlspecialchars($value ?? '', $flag | ENT_SUBSTITUTE, 'UTF-8', $doubleEncode);
}

/**
* Get the HTML string.
*
* @return string
*/
#[\Override]
public function toHtml()
{
return (static::$encodeUsingFactory ?? function ($value, $doubleEncode) {
return static::convert($value, doubleEncode: $doubleEncode);
})($this->html, $this->doubleEncode);
}

/**
* Set the callable that will be used to encode the HTML strings.
*
* @param callable|null $factory
* @return void
*/
public static function encodeUsing(?callable $factory = null)
{
static::$encodeUsingFactory = $factory;
}

/**
* Flush the class's global state.
*
* @return void
*/
public static function flushState()
{
static::$encodeUsingFactory = null;
}
}
22 changes: 22 additions & 0 deletions src/Illuminate/View/Compilers/BladeCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,28 @@ public function precompiler(callable $precompiler)
$this->precompilers[] = $precompiler;
}

/**
* Execute the given callback using a custom echo format.
*
* @param string $format
* @param callable $callback
* @return string
*/
public function usingEchoFormat($format, callable $callback)
{
$originalEchoFormat = $this->echoFormat;

$this->setEchoFormat($format);

try {
$output = call_user_func($callback);
} finally {
$this->setEchoFormat($originalEchoFormat);
}

return $output;
}

/**
* Set the echo format to be used by the compiler.
*
Expand Down
72 changes: 72 additions & 0 deletions tests/Integration/Mail/MailableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Illuminate\Tests\Integration\Mail;

use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Orchestra\Testbench\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class MailableTest extends TestCase
{
/** {@inheritdoc} */
#[\Override]
protected function defineEnvironment($app)
{
$app['view']->addLocation(__DIR__.'/Fixtures');
}

#[DataProvider('markdownEncodedDataProvider')]
public function testItCanAssertMarkdownEncodedString($given, $expected)
{
$mailable = new class($given) extends Mailable
{
public function __construct(public string $message)
{
//
}

public function envelope()
{
return new Envelope(
subject: 'My basic title',
);
}

public function content()
{
return new Content(
markdown: 'message',
);
}
};

$mailable->assertSeeInHtml($expected, false);
}

public static function markdownEncodedDataProvider()
{
yield ['[Laravel](https://laravel.com)', 'My message is: [Laravel](https://laravel.com)'];

yield [
'![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)',
'My message is: ![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)',
];

yield [
'Visit https://laravel.com/docs to browse the documentation',
'My message is: Visit https://laravel.com/docs to browse the documentation',
];

yield [
'Visit <https://laravel.com/docs> to browse the documentation',
'My message is: Visit &lt;https://laravel.com/docs&gt; to browse the documentation',
];

yield [
'Visit <span>https://laravel.com/docs</span> to browse the documentation',
'My message is: Visit &lt;span&gt;https://laravel.com/docs&lt;/span&gt; to browse the documentation',
];
}
}
74 changes: 74 additions & 0 deletions tests/Integration/Mail/MarkdownParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Illuminate\Tests\Integration\Mail;

use Illuminate\Mail\Markdown;
use Illuminate\Support\EncodedHtmlString;
use Illuminate\Support\HtmlString;
use Orchestra\Testbench\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class MarkdownParserTest extends TestCase
{
#[DataProvider('markdownDataProvider')]
public function testItCanParseMarkdownString($given, $expected)
{
tap(Markdown::parse($given), function ($html) use ($expected) {
$this->assertInstanceOf(HtmlString::class, $html);

$this->assertStringEqualsStringIgnoringLineEndings($expected.PHP_EOL, (string) $html);
$this->assertSame((string) $html, (string) $html->toHtml());
});
}

#[DataProvider('markdownEncodedDataProvider')]
public function testItCanParseMarkdownEncodedString($given, $expected)
{
tap(Markdown::parse($given), function ($html) use ($expected) {
$this->assertInstanceOf(HtmlString::class, $html);

$this->assertStringEqualsStringIgnoringLineEndings($expected.PHP_EOL, (string) $html);
});
}

public static function markdownDataProvider()
{
yield ['[Laravel](https://laravel.com)', '<p><a href="https://laravel.com">Laravel</a></p>'];
yield ['\[Laravel](https://laravel.com)', '<p>[Laravel](https://laravel.com)</p>'];
yield ['![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', '<p><img src="https://laravel.com/assets/img/welcome/background.svg" alt="Welcome to Laravel" /></p>'];
yield ['!\[Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', '<p>![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)</p>'];
yield ['Visit https://laravel.com/docs to browse the documentation', '<p>Visit https://laravel.com/docs to browse the documentation</p>'];
yield ['Visit <https://laravel.com/docs> to browse the documentation', '<p>Visit <a href="https://laravel.com/docs">https://laravel.com/docs</a> to browse the documentation</p>'];
yield ['Visit <span>https://laravel.com/docs</span> to browse the documentation', '<p>Visit <span>https://laravel.com/docs</span> to browse the documentation</p>'];
}

public static function markdownEncodedDataProvider()
{
yield [new EncodedHtmlString('[Laravel](https://laravel.com)'), '<p>[Laravel](https://laravel.com)</p>'];

yield [
new EncodedHtmlString('![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)'),
'<p>![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)</p>',
];

yield [
new EncodedHtmlString('Visit https://laravel.com/docs to browse the documentation'),
'<p>Visit https://laravel.com/docs to browse the documentation</p>',
];

yield [
new EncodedHtmlString('Visit <https://laravel.com/docs> to browse the documentation'),
'<p>Visit &lt;https://laravel.com/docs&gt; to browse the documentation</p>',
];

yield [
new EncodedHtmlString('Visit <span>https://laravel.com/docs</span> to browse the documentation'),
'<p>Visit &lt;span&gt;https://laravel.com/docs&lt;/span&gt; to browse the documentation</p>',
];

yield [
'![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)<br />'.new EncodedHtmlString('Visit <span>https://laravel.com/docs</span> to browse the documentation'),
'<p><img src="https://laravel.com/assets/img/welcome/background.svg" alt="Welcome to Laravel" /><br />Visit &lt;span&gt;https://laravel.com/docs&lt;/span&gt; to browse the documentation</p>',
];
}
}
Loading
Loading