diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index bbcc48bc6f..3f35217c41 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -319,6 +319,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\EmailEditor\Engine\Renderer\BodyRenderer::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Engine\Renderer\BlocksRenderer::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Engine\Renderer\BlocksRegistry::class)->setPublic(true); + $container->autowire(\MailPoet\EmailEditor\Engine\Renderer\Preprocessor::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Integrations\Core\Initializer::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 01d532215e..35303aeff7 100644 --- a/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php @@ -13,10 +13,7 @@ class BodyRenderer { $this->blocksRenderer = $blocksRenderer; } - public function renderBody(string $postContent): string { - $parser = new \WP_Block_Parser(); - $parsedBlocks = $parser->parse($postContent); - // @todo We need to wrap top level blocks which are not in columns into a column + public function renderBody(array $parsedBlocks): string { return $this->blocksRenderer->render($parsedBlocks); } } diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/Preprocessor.php b/mailpoet/lib/EmailEditor/Engine/Renderer/Preprocessor.php new file mode 100644 index 0000000000..c35bf8d80d --- /dev/null +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/Preprocessor.php @@ -0,0 +1,52 @@ + 'core/columns', + 'attrs' => [], + 'innerBlocks' => [[ + 'blockName' => 'core/column', + 'attrs' => [], + 'innerBlocks' => [], + ]], + ]; + + public function preprocess(array $parsedBlocks): array { + return $this->addTopLevelColumns($parsedBlocks); + } + + /** + * In the editor we allow putting content blocks directly into the root level of the email. + * But for rendering purposes it is more convenient to have them wrapped in a single column. + * This method walks through the first level of blocks and wraps non column blocks into a single column. + */ + private function addTopLevelColumns(array $parsedBlocks): array { + $wrappedParsedBlocks = []; + $nonColumnsBlocksBuffer = []; + foreach ($parsedBlocks as $block) { + // The next block is columns so we can flush the buffer and add the columns block + if ($block['blockName'] === 'core/columns') { + if ($nonColumnsBlocksBuffer) { + $columnsBlock = self::SINGLE_COLUMN_TEMPLATE; + $columnsBlock['innerBlocks'][0]['innerBlocks'] = $nonColumnsBlocksBuffer; + $nonColumnsBlocksBuffer = []; + $wrappedParsedBlocks[] = $columnsBlock; + } + $wrappedParsedBlocks[] = $block; + continue; + } + // Non columns block so we add it to the buffer + $nonColumnsBlocksBuffer[] = $block; + } + // Flush the buffer if there are any blocks left + if ($nonColumnsBlocksBuffer) { + $columnsBlock = self::SINGLE_COLUMN_TEMPLATE; + $columnsBlock['innerBlocks'][0]['innerBlocks'] = $nonColumnsBlocksBuffer; + $wrappedParsedBlocks[] = $columnsBlock; + } + return $wrappedParsedBlocks; + } +} diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php b/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php index c21ac6dbae..bd940337af 100644 --- a/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php @@ -13,6 +13,9 @@ class Renderer { /** @var BodyRenderer */ private $bodyRenderer; + /** @var Preprocessor */ + private $preprocessor; + const TEMPLATE_FILE = 'template.html'; const STYLES_FILE = 'styles.css'; @@ -21,14 +24,21 @@ class Renderer { */ public function __construct( \MailPoetVendor\CSS $cssInliner, + Preprocessor $preprocessor, BodyRenderer $bodyRenderer ) { $this->cssInliner = $cssInliner; + $this->preprocessor = $preprocessor; $this->bodyRenderer = $bodyRenderer; } public function render(\WP_Post $post, string $subject, string $preHeader, string $language, $metaRobots = ''): array { - $renderedBody = $this->bodyRenderer->renderBody($post->post_content); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps + $parser = new \WP_Block_Parser(); + $parsedBlocks = $parser->parse($post->post_content); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps + + $parsedBlocks = $this->preprocessor->preprocess($parsedBlocks); + $renderedBody = $this->bodyRenderer->renderBody($parsedBlocks); + $styles = (string)file_get_contents(dirname(__FILE__) . '/' . self::STYLES_FILE); $styles = apply_filters('mailpoet_email_renderer_styles', $styles, $post); diff --git a/mailpoet/tests/unit/EmailEditor/Engine/Renderer/PreprocessorTest.php b/mailpoet/tests/unit/EmailEditor/Engine/Renderer/PreprocessorTest.php new file mode 100644 index 0000000000..d940f1e7f8 --- /dev/null +++ b/mailpoet/tests/unit/EmailEditor/Engine/Renderer/PreprocessorTest.php @@ -0,0 +1,63 @@ + 'core/paragraph', + 'attrs' => [], + 'innerHTML' => 'Paragraph content', + ]; + + private $columnsBlock = [ + 'blockName' => 'core/columns', + 'attrs' => [], + 'innerBlocks' => [[ + 'blockName' => 'core/column', + 'attrs' => [], + 'innerBlocks' => [], + ]], + ]; + + /** @var Preprocessor */ + private $preprocessor; + + public function _before() { + parent::_before(); + $this->preprocessor = new Preprocessor(); + } + + public function testItWrapsSingleTopLevelBlockIntoColumns() { + $parsedDocument = [$this->paragraphBlock]; + $result = $this->preprocessor->preprocess($parsedDocument); + expect($result[0]['blockName'])->equals('core/columns'); + expect($result[0]['innerBlocks'][0]['blockName'])->equals('core/column'); + expect($result[0]['innerBlocks'][0]['innerBlocks'][0]['blockName'])->equals('core/paragraph'); + expect($result[0]['innerBlocks'][0]['innerBlocks'][0]['innerHTML'])->equals('Paragraph content'); + } + + public function testItDoesntWrapColumns() { + $parsedDocumentWithMultipleColumns = [$this->columnsBlock, $this->columnsBlock]; + $result = $this->preprocessor->preprocess($parsedDocumentWithMultipleColumns); + expect($result)->equals($parsedDocumentWithMultipleColumns); + } + + public function testItWrapsTopLevelBlocksSpreadBetweenColumns() { + $parsedDocument = [$this->paragraphBlock, $this->columnsBlock, $this->paragraphBlock, $this->paragraphBlock]; + // We expect to wrap top level paragraph blocks into columns so the result should three columns blocks + $result = $this->preprocessor->preprocess($parsedDocument); + expect($result)->count(3); + // First columns contain columns with one paragraph block + expect($result[0]['innerBlocks'][0]['blockName'])->equals('core/column'); + expect($result[0]['innerBlocks'][0]['innerBlocks'][0]['blockName'])->equals('core/paragraph'); + // Second columns remains empty + expect($result[1]['innerBlocks'][0]['blockName'])->equals('core/column'); + expect($result[1]['innerBlocks'][0]['innerBlocks'])->isEmpty(); + // Third columns contain columns with two paragraph blocks + expect($result[2]['innerBlocks'][0]['blockName'])->equals('core/column'); + expect($result[2]['innerBlocks'][0]['innerBlocks'])->count(2); + expect($result[2]['innerBlocks'][0]['innerBlocks'][0]['blockName'])->equals('core/paragraph'); + expect($result[2]['innerBlocks'][0]['innerBlocks'][1]['blockName'])->equals('core/paragraph'); + } +}