Add email renderer and template to the renderer engine

In this commit, I copied the code for processing the rendering of emails
from the current renderer.
This will allow us to use different base templates and styles.
Ideally, we should be able to add hooks and reuse the renderer from the engine namespace in
the current renderer.
[MAILPOET-5540]
This commit is contained in:
Rostislav Wolny
2023-08-24 14:01:43 +02:00
committed by Jan Lysý
parent b70ad064c7
commit 46a481ec24
8 changed files with 302 additions and 66 deletions

View File

@ -315,6 +315,7 @@ class ContainerConfigurator implements IContainerConfigurator {
// Email Editor // Email Editor
$container->autowire(\MailPoet\EmailEditor\Engine\EmailEditor::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Engine\EmailEditor::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\AssetsCleaner::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Engine\AssetsCleaner::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Renderer\Renderer::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Renderer\BodyRenderer::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Engine\Renderer\BodyRenderer::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailEditor::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailEditor::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailApiController::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailApiController::class)->setPublic(true);

View File

@ -3,11 +3,11 @@
namespace MailPoet\EmailEditor\Engine\Renderer; namespace MailPoet\EmailEditor\Engine\Renderer;
class BodyRenderer { class BodyRenderer {
public function renderBody(\WP_Post $emailPost): string { public function renderBody(string $postContent): string {
// @todo Parse blocks \WP_Block_Parser // @todo Parse blocks \WP_Block_Parser
// @todo We need to wrap top level blocks which are not in columns into a column // @todo We need to wrap top level blocks which are not in columns into a column
// @todo Add rendering of columns (inspire by/reuse code from mailpoet/lib/Newsletter/Renderer/Columns/Renderer) // @todo Add rendering of columns (inspire by/reuse code from mailpoet/lib/Newsletter/Renderer/Columns/Renderer)
// @todo Add rendering of blocks // @todo Add rendering of blocks
return $emailPost->post_content ?: ''; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps return $postContent ?: '';
} }
} }

View File

@ -0,0 +1,99 @@
<?php declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer;
use MailPoet\Util\pQuery\DomNode;
use MailPoetVendor\Html2Text\Html2Text;
class Renderer {
/** @var \MailPoetVendor\CSS */
private $cssInliner;
/** @var BodyRenderer */
private $bodyRenderer;
const TEMPLATE_FILE = 'template.html';
const STYLES_FILE = 'styles.css';
/**
* @param \MailPoetVendor\CSS $cssInliner
*/
public function __construct(
\MailPoetVendor\CSS $cssInliner,
BodyRenderer $bodyRenderer
) {
$this->cssInliner = $cssInliner;
$this->bodyRenderer = $bodyRenderer;
}
public function render(string $postContent, string $subject, string $preHeader, string $language, $metaRobots = ''): array {
$renderedBody = $this->bodyRenderer->renderBody($postContent);
$styles = (string)file_get_contents(dirname(__FILE__) . '/' . self::STYLES_FILE);
/**
* {{email_language}}
* {{email_subject}}
* {{email_meta_robots}}
* {{email_styles}}
* {{email_preheader}}
* {{email_body}}
*/
$templateWithContents = $this->injectContentIntoTemplate(
(string)file_get_contents(dirname(__FILE__) . '/' . self::TEMPLATE_FILE),
[
$language,
esc_html($subject),
$metaRobots,
$styles,
esc_html($preHeader),
$renderedBody,
]
);
$templateWithContentsDom = $this->inlineCSSStyles($templateWithContents);
$templateWithContents = $this->postProcessTemplate($templateWithContentsDom);
return [
'html' => $templateWithContents,
'text' => $this->renderTextVersion($templateWithContents),
];
}
private function injectContentIntoTemplate($template, array $content) {
return preg_replace_callback('/{{\w+}}/', function($matches) use (&$content) {
return array_shift($content);
}, $template);
}
/**
* @param string $template
* @return DomNode
*/
private function inlineCSSStyles($template) {
return $this->cssInliner->inlineCSS($template);
}
/**
* @param string $template
* @return string
*/
private function renderTextVersion($template) {
$template = (mb_detect_encoding($template, 'UTF-8', true)) ? $template : mb_convert_encoding($template, 'UTF-8', mb_list_encodings());
return @Html2Text::convert($template);
}
/**
* @param DomNode $templateDom
* @return string
*/
private function postProcessTemplate(DomNode $templateDom) {
// replace spaces in image tag URLs
foreach ($templateDom->query('img') as $image) {
$image->src = str_replace(' ', '%20', $image->src);
}
// because tburry/pquery contains a bug and replaces the opening non mso condition incorrectly we have to replace the opening tag with correct value
$template = $templateDom->__toString();
$template = str_replace('<!--[if !mso]><![endif]-->', '<!--[if !mso]><!-- -->', $template);
return $template;
}
}

View File

@ -0,0 +1,61 @@
/* Base CSS rules to be applied to all emails */
/* Created based on original MailPoet template for rendering emails */
/* StyleLint is disabled because some rules contain properties that linter marks as unknown, but they are valid for email rendering */
/* stylelint-disable property-no-unknown */
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%; /* From MJMJ - Automatic test adjustment on mobile max to 100% */
-ms-text-size-adjust: 100%; /* From MJMJ - Automatic test adjustment on mobile max to 100% */
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0;
mso-table-rspace: 0;
}
img {
border: 0;
height: auto;
-ms-interpolation-mode: bicubic;
line-height: 100%;
outline: none;
text-decoration: none;
}
p {
display: block;
margin: 13px 0;
}
/* https://www.emailonacid.com/blog/article/email-development/tips-for-coding-email-preheaders */
.email_preheader,
.email_preheader * {
color: #fff;
display: none;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
mso-hide: all;
opacity: 0;
overflow: hidden;
visibility: hidden;
}
@media screen and (max-width: 480px) {
.email_button {
width: 100% !important;
}
}
@media screen and (max-width: 599px) {
.email_button {
box-sizing: border-box !important;
padding: 5px 0 !important;
width: 100% !important;
}
}
/* stylelint-enable property-no-unknown */

View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html
lang="{{newsletter_language}}"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<title>{{newsletter_subject}}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="format-detection" content="telephone=no" />
{{email_meta_robots}}
<style type="text/css">
{{email_styles}}
</style>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table
class="email_template"
border="0"
width="100%"
cellpadding="0"
cellspacing="0"
style="border-spacing: 0; mso-table-lspace: 0; mso-table-rspace: 0"
>
<tbody>
<tr>
<td
class="email_preheader"
style="
-webkit-text-size-adjust: none;
font-size: 1px;
line-height: 1px;
color: #333333;
"
height="1"
>
{{email_preheader}}
</td>
</tr>
<tr>
<td align="center" class="email-wrapper" valign="top">
<!--[if mso]>
<table align="center" border="0" cellspacing="0" cellpadding="0"
width="660">
<tr>
<td class="email-wrapper" align="center" valign="top" width="660">
<![endif]-->
<table
class="email_content_wrapper"
border="0"
width="660"
cellpadding="0"
cellspacing="0"
style="
border-spacing: 0;
mso-table-lspace: 0;
mso-table-rspace: 0;
max-width: 660px;
width: 100%;
"
>
<tbody>
{{email_body}}
</tbody>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -4,7 +4,7 @@ namespace MailPoet\Newsletter\Renderer;
use MailPoet\Config\Env; use MailPoet\Config\Env;
use MailPoet\Config\ServicesChecker; use MailPoet\Config\ServicesChecker;
use MailPoet\EmailEditor\Engine\Renderer\BodyRenderer as GuntenbergBodyRenderer; use MailPoet\EmailEditor\Engine\Renderer\Renderer as GuntenbergRenderer;
use MailPoet\Entities\NewsletterEntity; use MailPoet\Entities\NewsletterEntity;
use MailPoet\Features\FeaturesController; use MailPoet\Features\FeaturesController;
use MailPoet\Logging\LoggerFactory; use MailPoet\Logging\LoggerFactory;
@ -24,8 +24,8 @@ class Renderer {
/** @var BodyRenderer */ /** @var BodyRenderer */
private $bodyRenderer; private $bodyRenderer;
/** @var GuntenbergBodyRenderer */ /** @var GuntenbergRenderer */
private $guntenbergBodyRenderer; private $guntenbergRenderer;
/** @var Preprocessor */ /** @var Preprocessor */
private $preprocessor; private $preprocessor;
@ -53,7 +53,7 @@ class Renderer {
public function __construct( public function __construct(
BodyRenderer $bodyRenderer, BodyRenderer $bodyRenderer,
GuntenbergBodyRenderer $guntenbergBodyRenderer, GuntenbergRenderer $guntenbergRenderer,
Preprocessor $preprocessor, Preprocessor $preprocessor,
\MailPoetVendor\CSS $cSSInliner, \MailPoetVendor\CSS $cSSInliner,
ServicesChecker $servicesChecker, ServicesChecker $servicesChecker,
@ -64,7 +64,7 @@ class Renderer {
FeaturesController $featuresController FeaturesController $featuresController
) { ) {
$this->bodyRenderer = $bodyRenderer; $this->bodyRenderer = $bodyRenderer;
$this->guntenbergBodyRenderer = $guntenbergBodyRenderer; $this->guntenbergRenderer = $guntenbergRenderer;
$this->preprocessor = $preprocessor; $this->preprocessor = $preprocessor;
$this->cSSInliner = $cSSInliner; $this->cSSInliner = $cSSInliner;
$this->servicesChecker = $servicesChecker; $this->servicesChecker = $servicesChecker;
@ -84,72 +84,69 @@ class Renderer {
} }
private function _render(NewsletterEntity $newsletter, SendingTask $sendingTask = null, $type = false, $preview = false, $subject = null) { private function _render(NewsletterEntity $newsletter, SendingTask $sendingTask = null, $type = false, $preview = false, $subject = null) {
$body = (is_array($newsletter->getBody()))
? $newsletter->getBody()
: [];
$content = (array_key_exists('content', $body))
? $body['content']
: [];
$styles = (array_key_exists('globalStyles', $body))
? $body['globalStyles']
: [];
if (
!$this->servicesChecker->isUserActivelyPaying() && !$preview
) {
$content = $this->addMailpoetLogoContentBlock($content, $styles);
}
$language = $this->wp->getBloginfo('language'); $language = $this->wp->getBloginfo('language');
$metaRobots = $preview ? '<meta name="robots" content="noindex, nofollow" />' : ''; $metaRobots = $preview ? '<meta name="robots" content="noindex, nofollow" />' : '';
$renderedBody = ""; $subject = $subject ?: $newsletter->getSubject();
try { $wpPost = $newsletter->getWpPost();
$content = $this->preprocessor->process($newsletter, $content, $preview, $sendingTask); if ($this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR) && $wpPost instanceof \WP_Post) {
if ($this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR) && $newsletter->getWpPostId()) { $renderedNewsletter = $this->guntenbergRenderer->render($wpPost->post_content, $subject, $newsletter->getPreheader(), $language, $metaRobots); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$post = $newsletter->getWpPost(); } else {
if (!$post instanceof \WP_Post) { $body = (is_array($newsletter->getBody()))
throw new NewsletterProcessingException('Missing email post object'); ? $newsletter->getBody()
} : [];
$renderedBody = $this->guntenbergBodyRenderer->renderBody($post); $content = (array_key_exists('content', $body))
} else { ? $body['content']
: [];
$styles = (array_key_exists('globalStyles', $body))
? $body['globalStyles']
: [];
if (
!$this->servicesChecker->isUserActivelyPaying() && !$preview
) {
$content = $this->addMailpoetLogoContentBlock($content, $styles);
}
$renderedBody = "";
try {
$content = $this->preprocessor->process($newsletter, $content, $preview, $sendingTask);
$renderedBody = $this->bodyRenderer->renderBody($newsletter, $content); $renderedBody = $this->bodyRenderer->renderBody($newsletter, $content);
} catch (NewsletterProcessingException $e) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_COUPONS)->error(
$e->getMessage(),
['newsletter_id' => $newsletter->getId()]
);
$this->newslettersRepository->setAsCorrupt($newsletter);
if ($newsletter->getLatestQueue()) {
$this->sendingQueuesRepository->pause($newsletter->getLatestQueue());
}
} }
$renderedStyles = $this->renderStyles($styles);
$customFontsLinks = StylesHelper::getCustomFontsLinks($styles);
} catch (NewsletterProcessingException $e) { $template = $this->injectContentIntoTemplate(
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_COUPONS)->error( (string)file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE),
$e->getMessage(), [
['newsletter_id' => $newsletter->getId()] $language,
$metaRobots,
htmlspecialchars($subject),
$renderedStyles,
$customFontsLinks,
EHelper::escapeHtmlText($newsletter->getPreheader()),
$renderedBody,
]
); );
$this->newslettersRepository->setAsCorrupt($newsletter); if ($template === null) {
if ($newsletter->getLatestQueue()) { $template = '';
$this->sendingQueuesRepository->pause($newsletter->getLatestQueue());
} }
} $templateDom = $this->inlineCSSStyles($template);
$renderedStyles = $this->renderStyles($styles); $template = $this->postProcessTemplate($templateDom);
$customFontsLinks = StylesHelper::getCustomFontsLinks($styles);
$template = $this->injectContentIntoTemplate( $renderedNewsletter = [
(string)file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE), 'html' => $template,
[ 'text' => $this->renderTextVersion($template),
$language, ];
$metaRobots,
htmlspecialchars($subject ?: $newsletter->getSubject()),
$renderedStyles,
$customFontsLinks,
EHelper::escapeHtmlText($newsletter->getPreheader()),
$renderedBody,
]
);
if ($template === null) {
$template = '';
} }
$templateDom = $this->inlineCSSStyles($template);
$template = $this->postProcessTemplate($templateDom);
$renderedNewsletter = [
'html' => $template,
'text' => $this->renderTextVersion($template),
];
return ($type && !empty($renderedNewsletter[$type])) ? return ($type && !empty($renderedNewsletter[$type])) ?
$renderedNewsletter[$type] : $renderedNewsletter[$type] :

View File

@ -55,7 +55,7 @@ class RendererTest extends \MailPoetTest {
$this->servicesChecker = $this->createMock(ServicesChecker::class); $this->servicesChecker = $this->createMock(ServicesChecker::class);
$this->renderer = new Renderer( $this->renderer = new Renderer(
$this->diContainer->get(BodyRenderer::class), $this->diContainer->get(BodyRenderer::class),
$this->diContainer->get(\MailPoet\EmailEditor\Engine\Renderer\BodyRenderer::class), $this->diContainer->get(\MailPoet\EmailEditor\Engine\Renderer\Renderer::class),
$this->diContainer->get(Preprocessor::class), $this->diContainer->get(Preprocessor::class),
$this->diContainer->get(\MailPoetVendor\CSS::class), $this->diContainer->get(\MailPoetVendor\CSS::class),
$this->servicesChecker, $this->servicesChecker,

View File

@ -139,7 +139,7 @@ class RendererTest extends \MailPoetTest {
)); ));
return new NewsletterRenderer( return new NewsletterRenderer(
$this->diContainer->get(\MailPoet\Newsletter\Renderer\BodyRenderer::class), $this->diContainer->get(\MailPoet\Newsletter\Renderer\BodyRenderer::class),
$this->diContainer->get(\MailPoet\EmailEditor\Engine\Renderer\BodyRenderer::class), $this->diContainer->get(\MailPoet\EmailEditor\Engine\Renderer\Renderer::class),
new Preprocessor( new Preprocessor(
$this->diContainer->get(\MailPoet\Newsletter\Renderer\Blocks\AbandonedCartContent::class), $this->diContainer->get(\MailPoet\Newsletter\Renderer\Blocks\AbandonedCartContent::class),
$this->diContainer->get(\MailPoet\Newsletter\Renderer\Blocks\AutomatedLatestContentBlock::class), $this->diContainer->get(\MailPoet\Newsletter\Renderer\Blocks\AutomatedLatestContentBlock::class),