8000 feature #47201 [Mime] Add a way to control the HTML to text conversio… · symfony/symfony@475d4c0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 475d4c0

Browse files
committed
feature #47201 [Mime] Add a way to control the HTML to text conversion (fabpot)
This PR was squashed before being merged into the 6.2 branch. Discussion ---------- [Mime] Add a way to control the HTML to text conversion | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | Fix #46295 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT | Doc PR | Commits ------- f3789b3 [Mime] Add a way to control the HTML to text conversion
2 parents a96ccca + f3789b3 commit 475d4c0

10 files changed

+166
-30
lines changed

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,22 +126,23 @@
126126
"doctrine/data-fixtures": "^1.1",
127127
"doctrine/dbal": "^2.13.1|^3.0",
128128
"doctrine/orm": "^2.7.4",
129+
"egulias/email-validator": "^2.1.10|^3.1",
129130
"guzzlehttp/promises": "^1.4",
131+
"league/html-to-markdown": "^5.0",
130132
"masterminds/html5": "^2.7.2",
131133
"monolog/monolog": "^1.25.1|^2",
132134
"nyholm/psr7": "^1.0",
133135
"pda/pheanstalk": "^4.0",
134136
"php-http/httplug": "^1.0|^2.0",
137+
"phpdocumentor/reflection-docblock": "^5.2",
135138
"phpstan/phpdoc-parser": "^1.0",
136139
"predis/predis": "~1.1",
137140
"psr/http-client": "^1.0",
138141
"psr/simple-cache": "^1.0|^2.0|^3.0",
139-
"egulias/email-validator": "^2.1.10|^3.1",
140142
"symfony/mercure-bundle": "^0.3",
141143
"symfony/phpunit-bridge": "^5.4|^6.0",
142144
"symfony/runtime": "self.version",
143145
"symfony/security-acl": "~2.8|~3.0",
144-
"phpdocumentor/reflection-docblock": "^5.2",
145146
"twig/cssinliner-extra": "^2.12|^3",
146147
"twig/inky-extra": "^2.12|^3",
147148
"twig/markdown-extra": "^2.12|^3"

src/Symfony/Bridge/Twig/Mime/BodyRenderer.php

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111

1212
namespace Symfony\Bridge\Twig\Mime;
1313

14-
use League\HTMLToMarkdown\HtmlConverter;
14+
use League\HTMLToMarkdown\HtmlConverterInterface;
1515
use Symfony\Component\Mime\BodyRendererInterface;
1616
use Symfony\Component\Mime\Exception\InvalidArgumentException;
17+
use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter;
18+
use Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface;
19+
use Symfony\Component\Mime\HtmlToTextConverter\LeagueHtmlToMarkdownConverter;
1720
use Symfony\Component\Mime\Message;
1821
use Twig\Environment;
1922

@@ -24,19 +27,13 @@ final class BodyRenderer implements BodyRendererInterface
2427
{
2528
private Environment $twig;
2629
private array $context;
27-
private HtmlConverter $converter;
30+
private HtmlToTextConverterInterface $converter;
2831

29-
public function __construct(Environment $twig, array $context = [])
32+
public function __construct(Environment $twig, array $context = [], HtmlToTextConverterInterface $converter = null)
3033
{
3134
$this->twig = $twig;
3235
$this->context = $context;
33-
if (class_exists(HtmlConverter::class)) {
34-
$this->converter = new HtmlConverter([
35-
'hard_break' => true,
36-
'strip_tags' => true,
37-
'remove_nodes' => 'head style',
38-
]);
39-
}
36+
$this->converter = $converter ?: (interface_exists(HtmlConverterInterface::class) ? new LeagueHtmlToMarkdownConverter() : new DefaultHtmlToTextConverter());
4037
}
4138

4239
public function render(Message $message): void
@@ -74,16 +71,8 @@ public function render(Message $message): void
7471

7572
// if text body is empty, compute one from the HTML body
7673
if (!$message->getTextBody() && null !== $html = $message->getHtmlBody()) {
77-
$message->text($this->convertHtmlToText(\is_resource($html) ? stream_get_contents($html) : $html));
78-
}
79-
}
80-
81-
private function convertHtmlToText(string $html): string
82-
{
83-
if (isset($this->converter)) {
84-
return $this->converter->convert($html);
74+
$text = $this->converter->convert(\is_resource($html) ? stream_get_contents($html) : $html, $message->getHtmlCharset());
75+
$message->text($text, $message->getHtmlCharset());
8576
}
86-
87-
return strip_tags(preg_replace('{<(head|style)\b.*?</\1>}is', '', $html));
8877
}
8978
}

src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Symfony\Bridge\Twig\Mime\BodyRenderer;
1616
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
1717
use Symfony\Component\Mime\Exception\InvalidArgumentException;
18+
use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter;
19+
use Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface;
1820
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
1921
use Twig\Environment;
2022
use Twig\Loader\ArrayLoader;
@@ -27,14 +29,24 @@ public function testRenderTextOnly()
2729
$this->assertEquals('Text', $email->getBody()->bodyToString());
2830
}
2931

30-
public function testRenderHtmlOnly()
32+
public function testRenderHtmlOnlyWithDefaultConverter()
3133
{
32-
$html = '<head>head</head><b>HTML</b><style type="text/css">css</style>';
33-
$email = $this->prepareEmail(null, $html);
34+
$html = '<head><meta charset="utf-8"></head><b>HTML</b><style>css</style>';
35+
$email = $this->prepareEmail(null, $html, [], new DefaultHtmlToTextConverter());
3436
$body = $email->getBody();
3537
$this->assertInstanceOf(AlternativePart::class, $body);
3638
$this->assertEquals('HTML', $body->getParts()[0]->bodyToString());
37-
$this->assertEquals(str_replace('=', '=3D', $html), $body->getParts()[1]->bodyToString());
39+
$this->assertEquals(str_replace(['=', "\n"], ['=3D', "\r\n"], $html), $body->getParts()[1]->bodyToString());
40+
}
41+
42+
public function testRenderHtmlOnlyWithLeagueConverter()
43+
{
44+
$html = '<head><meta charset="utf-8"></head><b>HTML</b><style>css</style>';
45+
$email = $this->prepareEmail(null, $html);
46+
$body = $email->getBody();
47+
$this->assertInstanceOf(AlternativePart::class, $body);
48+
$this->assertEquals('**HTML**', $body->getParts()[0]->bodyToString());
49+
$this->assertEquals(str_replace(['=', "\n"], ['=3D', "\r\n"], $html), $body->getParts()[1]->bodyToString());
3850
}
3951

4052
public function testRenderMultiLineHtmlOnly()
@@ -50,7 +62,7 @@ public function testRenderMultiLineHtmlOnly()
5062
$email = $this->prepareEmail(null, $html);
5163
$body = $email->getBody();
5264
$this->assertInstanceOf(AlternativePart::class, $body);
53-
$this->assertEquals('HTML', str_replace(["\r", "\n"], '', $body->getParts()[0]->bodyToString()));
65+
$this->assertEquals('**HTML**', str_replace(["\r", "\n"], '', $body->getParts()[0]->bodyToString()));
5466
$this->assertEquals(str_replace(['=', "\n"], ['=3D', "\r\n"], $html), $body->getParts()[1]->bodyToString());
5567
}
5668

@@ -121,15 +133,15 @@ public function testRenderedOnceUnserializableContext()
121133
$this->assertEquals('Text', $email->getTextBody());
122134
}
123135

124-
private function prepareEmail(?string $text, ?string $html, array $context = []): TemplatedEmail
136+
private function prepareEmail(?string $text, ?string $html, array $context = [], HtmlToTextConverterInterface $converter = null): TemplatedEmail
125137
{
126138
$twig = new Environment(new ArrayLoader([
127139
'text' => $text,
128140
'html' => $html,
129141
'document.txt' => 'Some text document...',
130142
'image.jpg' => 'Some image data',
131143
]));
132-
$renderer = new BodyRenderer($twig);
144+
$renderer = new BodyRenderer($twig, [], $converter);
133145
$email = (new TemplatedEmail())
134146
->to('fabien@symfony.com')
135147
->from('helene@symfony.com')

src/Symfony/Bridge/Twig/composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"require-dev": {
2424
"doctrine/annotations": "^1.12",
2525
"egulias/email-validator": "^2.1.10|^3",
26+
"league/html-to-markdown": "^5.0",
2627
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
2728
"symfony/asset": "^5.4|^6.0",
2829
"symfony/dependency-injection": "^5.4|^6.0",
@@ -32,7 +33,7 @@
3233
"symfony/http-foundation": "^5.4|^6.0",
3334
"symfony/http-kernel": "^6.2",
3435
"symfony/intl": "^5.4|^6.0",
35-
"symfony/mime": "^5.4|^6.0",
36+
"symfony/mime": "^6.2",
3637
"symfony/polyfill-intl-icu": "~1.0",
3738
"symfony/property-info": "^5.4|^6.0",
3839
"symfony/routing": "^5.4|^6.0",
@@ -59,6 +60,7 @@
5960
"symfony/form": "<6.1",
6061
"symfony/http-foundation": "<5.4",
6162
"symfony/http-kernel": "<6.2",
63+
"symfony/mime": "<6.2",
6264
"symfony/translation": "<5.4",
6365
"symfony/workflow": "<5.4"
6466
},
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source cod F438 e.
10+
*/
11+
12+
namespace Symfony\Component\Mime\HtmlToTextConverter;
13+
14+
/**
15+
* @author Fabien Potencier <fabien@symfony.com>
16+
*/
17+
class DefaultHtmlToTextConverter implements HtmlToTextConverterInterface
18+
{
19+
public function convert(string $html, string $charset): string
20+
{
21+
return strip_tags(preg_replace('{<(head|style)\b.*?</\1>}is', '', $html));
22+
}
23+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\HtmlToTextConverter;
13+
14+
/**
15+
* @author Fabien Potencier <fabien@symfony.com>
16+
*/
17+
interface HtmlToTextConverterInterface
18+
{
19+
/**
20+
* Converts and HTML representation of a Message to a text representation.
21+
*
22+
* The output must use the same charset as the HTML one.
23+
*/
24+
public function convert(string $html, string $charset): string;
25+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\HtmlToTextConverter;
13+
14+
use League\HTMLToMarkdown\HtmlConverter;
15+
use League\HTMLToMarkdown\HtmlConverterInterface;
16+
17+
/**
18+
* @author Fabien Potencier <fabien@symfony.com>
19+
*/
20+
class LeagueHtmlToMarkdownConverter implements HtmlToTextConverterInterface
21+
{
22+
public function __construct(
23+
private HtmlConverterInterface $converter = new HtmlConverter([
24+
'hard_break' => true,
25+
'strip_tags' => true,
26+
'remove_nodes' => 'head style',
27+
]),
28+
) {
29+
}
30+
31+
public function convert(string $html, string $charset): string
32+
{
33+
return $this->converter->convert($html);
34+
}
35+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\Tests\HtmlToTextConverter;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter;
16+
17+
class DefaultHtmlToTextConverterTest extends TestCase
18+
{
19+
public function testConvert()
20+
{
21+
$converter = new DefaultHtmlToTextConverter();
22+
$this->assertSame('HTML', $converter->convert('<head><meta charset="utf-8"></head><b>HTML</b><style>css</style>', 'UTF-8'));
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mime\Tests\HtmlToTextConverter;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Mime\HtmlToTextConverter\LeagueHtmlToMarkdownConverter;
16+
17+
class LeagueHtmlToMarkdownConverterTest extends TestCase
18+
{
19+
public function testConvert()
20+
{
21+
$converter = new LeagueHtmlToMarkdownConverter();
22+
$this->assertSame('**HTML**', $converter->convert('<head><meta charset="utf-8"></head><b>HTML</b><style>css</style>', 'UTF-8'));
23+
}
24+
}

src/Symfony/Component/Mime/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
},
2323
"require-dev": {
2424
"egulias/email-validator": "^2.1.10|^3.1",
25+
"league/html-to-markdown": "^5.0",
2526
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
2627
"symfony/dependency-injection": "^5.4|^6.0",
2728
"symfony/property-access": "^5.4|^6.0",

0 commit comments

Comments
 (0)
0