From 46a481ec24f381ec93c20e73bacd606225194e86 Mon Sep 17 00:00:00 2001 From: Rostislav Wolny Date: Thu, 24 Aug 2023 14:01:43 +0200 Subject: [PATCH] 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] --- mailpoet/lib/DI/ContainerConfigurator.php | 1 + .../Engine/Renderer/BodyRenderer.php | 4 +- .../EmailEditor/Engine/Renderer/Renderer.php | 99 ++++++++++++++ .../EmailEditor/Engine/Renderer/styles.css | 61 +++++++++ .../EmailEditor/Engine/Renderer/template.html | 78 +++++++++++ mailpoet/lib/Newsletter/Renderer/Renderer.php | 121 +++++++++--------- .../integration/Newsletter/RendererTest.php | 2 +- .../TransactionalEmails/RendererTest.php | 2 +- 8 files changed, 302 insertions(+), 66 deletions(-) create mode 100644 mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php create mode 100644 mailpoet/lib/EmailEditor/Engine/Renderer/styles.css create mode 100644 mailpoet/lib/EmailEditor/Engine/Renderer/template.html diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index d00678ad29..29cd1d3e4c 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -315,6 +315,7 @@ class ContainerConfigurator implements IContainerConfigurator { // Email Editor $container->autowire(\MailPoet\EmailEditor\Engine\EmailEditor::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\Integrations\MailPoet\EmailEditor::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailApiController::class)->setPublic(true); diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php b/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php index 95543a960e..9885fe6b84 100644 --- a/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php @@ -3,11 +3,11 @@ namespace MailPoet\EmailEditor\Engine\Renderer; class BodyRenderer { - public function renderBody(\WP_Post $emailPost): string { + public function renderBody(string $postContent): string { // @todo Parse blocks \WP_Block_Parser // @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 blocks - return $emailPost->post_content ?: ''; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps + return $postContent ?: ''; } } diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php b/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php new file mode 100644 index 0000000000..2e3cccf788 --- /dev/null +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php @@ -0,0 +1,99 @@ +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('', '', $template); + return $template; + } +} diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/styles.css b/mailpoet/lib/EmailEditor/Engine/Renderer/styles.css new file mode 100644 index 0000000000..831b9cb444 --- /dev/null +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/styles.css @@ -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 */ diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/template.html b/mailpoet/lib/EmailEditor/Engine/Renderer/template.html new file mode 100644 index 0000000000..c118c97574 --- /dev/null +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/template.html @@ -0,0 +1,78 @@ + + + {{newsletter_subject}} + + + + + {{email_meta_robots}} + + + + + + + + + + + + +
+ + diff --git a/mailpoet/lib/Newsletter/Renderer/Renderer.php b/mailpoet/lib/Newsletter/Renderer/Renderer.php index 6cb6808146..3031a631c9 100644 --- a/mailpoet/lib/Newsletter/Renderer/Renderer.php +++ b/mailpoet/lib/Newsletter/Renderer/Renderer.php @@ -4,7 +4,7 @@ namespace MailPoet\Newsletter\Renderer; use MailPoet\Config\Env; 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\Features\FeaturesController; use MailPoet\Logging\LoggerFactory; @@ -24,8 +24,8 @@ class Renderer { /** @var BodyRenderer */ private $bodyRenderer; - /** @var GuntenbergBodyRenderer */ - private $guntenbergBodyRenderer; + /** @var GuntenbergRenderer */ + private $guntenbergRenderer; /** @var Preprocessor */ private $preprocessor; @@ -53,7 +53,7 @@ class Renderer { public function __construct( BodyRenderer $bodyRenderer, - GuntenbergBodyRenderer $guntenbergBodyRenderer, + GuntenbergRenderer $guntenbergRenderer, Preprocessor $preprocessor, \MailPoetVendor\CSS $cSSInliner, ServicesChecker $servicesChecker, @@ -64,7 +64,7 @@ class Renderer { FeaturesController $featuresController ) { $this->bodyRenderer = $bodyRenderer; - $this->guntenbergBodyRenderer = $guntenbergBodyRenderer; + $this->guntenbergRenderer = $guntenbergRenderer; $this->preprocessor = $preprocessor; $this->cSSInliner = $cSSInliner; $this->servicesChecker = $servicesChecker; @@ -84,72 +84,69 @@ class Renderer { } 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'); $metaRobots = $preview ? '' : ''; - $renderedBody = ""; - try { - $content = $this->preprocessor->process($newsletter, $content, $preview, $sendingTask); - if ($this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR) && $newsletter->getWpPostId()) { - $post = $newsletter->getWpPost(); - if (!$post instanceof \WP_Post) { - throw new NewsletterProcessingException('Missing email post object'); - } - $renderedBody = $this->guntenbergBodyRenderer->renderBody($post); - } else { + $subject = $subject ?: $newsletter->getSubject(); + $wpPost = $newsletter->getWpPost(); + if ($this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR) && $wpPost instanceof \WP_Post) { + $renderedNewsletter = $this->guntenbergRenderer->render($wpPost->post_content, $subject, $newsletter->getPreheader(), $language, $metaRobots); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps + } else { + $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); + } + + $renderedBody = ""; + try { + $content = $this->preprocessor->process($newsletter, $content, $preview, $sendingTask); $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) { - $this->loggerFactory->getLogger(LoggerFactory::TOPIC_COUPONS)->error( - $e->getMessage(), - ['newsletter_id' => $newsletter->getId()] + $template = $this->injectContentIntoTemplate( + (string)file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE), + [ + $language, + $metaRobots, + htmlspecialchars($subject), + $renderedStyles, + $customFontsLinks, + EHelper::escapeHtmlText($newsletter->getPreheader()), + $renderedBody, + ] ); - $this->newslettersRepository->setAsCorrupt($newsletter); - if ($newsletter->getLatestQueue()) { - $this->sendingQueuesRepository->pause($newsletter->getLatestQueue()); + if ($template === null) { + $template = ''; } - } - $renderedStyles = $this->renderStyles($styles); - $customFontsLinks = StylesHelper::getCustomFontsLinks($styles); + $templateDom = $this->inlineCSSStyles($template); + $template = $this->postProcessTemplate($templateDom); - $template = $this->injectContentIntoTemplate( - (string)file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE), - [ - $language, - $metaRobots, - htmlspecialchars($subject ?: $newsletter->getSubject()), - $renderedStyles, - $customFontsLinks, - EHelper::escapeHtmlText($newsletter->getPreheader()), - $renderedBody, - ] - ); - if ($template === null) { - $template = ''; + $renderedNewsletter = [ + 'html' => $template, + 'text' => $this->renderTextVersion($template), + ]; } - $templateDom = $this->inlineCSSStyles($template); - $template = $this->postProcessTemplate($templateDom); - - $renderedNewsletter = [ - 'html' => $template, - 'text' => $this->renderTextVersion($template), - ]; return ($type && !empty($renderedNewsletter[$type])) ? $renderedNewsletter[$type] : diff --git a/mailpoet/tests/integration/Newsletter/RendererTest.php b/mailpoet/tests/integration/Newsletter/RendererTest.php index 35be8e28f1..6fe313bddb 100644 --- a/mailpoet/tests/integration/Newsletter/RendererTest.php +++ b/mailpoet/tests/integration/Newsletter/RendererTest.php @@ -55,7 +55,7 @@ class RendererTest extends \MailPoetTest { $this->servicesChecker = $this->createMock(ServicesChecker::class); $this->renderer = new Renderer( $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(\MailPoetVendor\CSS::class), $this->servicesChecker, diff --git a/mailpoet/tests/integration/WooCommerce/TransactionalEmails/RendererTest.php b/mailpoet/tests/integration/WooCommerce/TransactionalEmails/RendererTest.php index 0a68ce9c1d..f56ee2e075 100644 --- a/mailpoet/tests/integration/WooCommerce/TransactionalEmails/RendererTest.php +++ b/mailpoet/tests/integration/WooCommerce/TransactionalEmails/RendererTest.php @@ -139,7 +139,7 @@ class RendererTest extends \MailPoetTest { )); return new NewsletterRenderer( $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( $this->diContainer->get(\MailPoet\Newsletter\Renderer\Blocks\AbandonedCartContent::class), $this->diContainer->get(\MailPoet\Newsletter\Renderer\Blocks\AutomatedLatestContentBlock::class),