mailpoet
/
vendor
/
woocommerce
/
email-editor
/
src
/
Engine
/
Renderer
/
ContentRenderer
/
class-content-renderer.php
mailpoet
/
vendor
/
woocommerce
/
email-editor
/
src
/
Engine
/
Renderer
/
ContentRenderer
Last commit date
Layout
4 days ago
Postprocessors
11 months ago
Preprocessors
2 weeks ago
class-block-renderer.php
11 months ago
class-blocks-parser.php
11 months ago
class-content-renderer.php
4 days ago
class-preset-variable-resolver.php
3 months ago
class-process-manager.php
3 months ago
class-rendering-context.php
3 months ago
content.css
11 months ago
index.php
11 months ago
class-content-renderer.php
349 lines
| 1 | <?php |
| 2 | declare(strict_types = 1); |
| 3 | namespace Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer; |
| 4 | if (!defined('ABSPATH')) exit; |
| 5 | use Automattic\WooCommerce\EmailEditor\Engine\Logger\Email_Editor_Logger; |
| 6 | use Automattic\WooCommerce\EmailEditor\Engine\Renderer\Css_Inliner; |
| 7 | use Automattic\WooCommerce\EmailEditor\Engine\Theme_Controller; |
| 8 | use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Fallback; |
| 9 | use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Post_Content; |
| 10 | use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper; |
| 11 | use WP_Block_Template; |
| 12 | use WP_Block_Type_Registry; |
| 13 | use WP_Post; |
| 14 | use WP_Style_Engine; |
| 15 | class Content_Renderer { |
| 16 | private Process_Manager $process_manager; |
| 17 | private Theme_Controller $theme_controller; |
| 18 | const CONTENT_STYLES_FILE = 'content.css'; |
| 19 | private WP_Block_Type_Registry $block_type_registry; |
| 20 | private $backup_template_content; |
| 21 | private $backup_template_id; |
| 22 | private $backup_post; |
| 23 | private $backup_query; |
| 24 | private Fallback $fallback_renderer; |
| 25 | private Email_Editor_Logger $logger; |
| 26 | private $backup_post_content_callback; |
| 27 | private ?string $post_content_width = null; |
| 28 | private array $container_padding = array(); |
| 29 | private Css_Inliner $css_inliner; |
| 30 | private ?Rendering_Context $rendering_context = null; |
| 31 | public function __construct( |
| 32 | Process_Manager $preprocess_manager, |
| 33 | Css_Inliner $css_inliner, |
| 34 | Theme_Controller $theme_controller, |
| 35 | Email_Editor_Logger $logger |
| 36 | ) { |
| 37 | $this->process_manager = $preprocess_manager; |
| 38 | $this->css_inliner = $css_inliner; |
| 39 | $this->theme_controller = $theme_controller; |
| 40 | $this->logger = $logger; |
| 41 | $this->block_type_registry = WP_Block_Type_Registry::get_instance(); |
| 42 | $this->fallback_renderer = new Fallback(); |
| 43 | } |
| 44 | private function initialize() { |
| 45 | add_filter( 'render_block', array( $this, 'render_block' ), 10, 2 ); |
| 46 | add_filter( 'block_parser_class', array( $this, 'block_parser' ) ); |
| 47 | add_filter( 'woocommerce_email_blocks_renderer_parsed_blocks', array( $this, 'preprocess_parsed_blocks' ) ); |
| 48 | // Swap core/post-content render callback for email rendering. |
| 49 | // This prevents issues with WordPress's static $seen_ids array when rendering |
| 50 | // multiple emails in a single request (e.g., MailPoet batch processing). |
| 51 | $post_content_type = $this->block_type_registry->get_registered( 'core/post-content' ); |
| 52 | if ( $post_content_type ) { |
| 53 | // Save the original callback (may be null or WordPress's default). |
| 54 | $this->backup_post_content_callback = $post_content_type->render_callback; |
| 55 | // Replace with our stateless renderer. |
| 56 | $post_content_renderer = new Post_Content(); |
| 57 | $post_content_type->render_callback = array( $post_content_renderer, 'render_stateless' ); |
| 58 | } |
| 59 | } |
| 60 | public function set_rendering_context( Rendering_Context $rendering_context ): void { |
| 61 | $this->rendering_context = $rendering_context; |
| 62 | } |
| 63 | public function get_current_rendering_context(): ?Rendering_Context { |
| 64 | return $this->rendering_context; |
| 65 | } |
| 66 | public function restore_rendering_context( ?Rendering_Context $rendering_context ): void { |
| 67 | $this->rendering_context = $rendering_context; |
| 68 | } |
| 69 | public function render( WP_Post $post, WP_Block_Template $template ): string { |
| 70 | $result = $this->render_without_css_inline( $post, $template ); |
| 71 | $styles = '<style>' . $result['styles'] . '</style>'; |
| 72 | $html = $this->css_inliner->from_html( $styles . $result['html'] )->inline_css()->render(); |
| 73 | return $this->process_manager->postprocess( $html ); |
| 74 | } |
| 75 | public function render_without_css_inline( WP_Post $post, WP_Block_Template $template ): array { |
| 76 | if ( null === $this->rendering_context ) { |
| 77 | $this->rendering_context = $this->create_rendering_context( null, $post, $template ); |
| 78 | } |
| 79 | $this->set_template_globals( $post, $template ); |
| 80 | $this->initialize(); |
| 81 | try { |
| 82 | do_action( 'woocommerce_email_editor_render_start' ); |
| 83 | $rendered_html = get_the_block_template_html(); |
| 84 | } finally { |
| 85 | $this->reset(); |
| 86 | } |
| 87 | return array( |
| 88 | 'html' => $rendered_html, |
| 89 | 'styles' => $this->collect_styles( $post, $template ), |
| 90 | ); |
| 91 | } |
| 92 | public function block_parser() { |
| 93 | return 'Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Blocks_Parser'; |
| 94 | } |
| 95 | public function preprocess_parsed_blocks( array $parsed_blocks ): array { |
| 96 | $styles = $this->theme_controller->get_styles(); |
| 97 | $layout = $this->theme_controller->get_layout_settings(); |
| 98 | // Pass the CSS variables map so preprocessors can resolve preset |
| 99 | // references (e.g. var:preset|spacing|20) in block attributes. |
| 100 | $styles['__variables_map'] = $this->theme_controller->get_variables_values_map(); |
| 101 | // Second pass (user blocks inside post-content): if root padding was |
| 102 | // applied to a container above post-content in the first pass (indicated |
| 103 | // by post_content_width < contentSize), remove root padding from styles |
| 104 | // to prevent double application. If the template delegates root padding |
| 105 | // (post_content_width == contentSize), keep it for user blocks. |
| 106 | if ( null !== $this->post_content_width ) { |
| 107 | $post_content_num = (float) str_replace( 'px', '', $this->post_content_width ); |
| 108 | $content_size_num = (float) str_replace( 'px', '', $layout['contentSize'] ); |
| 109 | // Use epsilon tolerance for floating-point comparison since width |
| 110 | // calculations involve round() and division that may produce imprecision. |
| 111 | if ( $post_content_num < $content_size_num - 0.01 ) { |
| 112 | unset( $styles['spacing']['padding']['left'], $styles['spacing']['padding']['right'] ); |
| 113 | // Constrain user blocks to the (inset) post-content width so they |
| 114 | // fit inside it instead of overflowing at the full contentSize. |
| 115 | $layout['contentSize'] = $this->post_content_width; |
| 116 | } |
| 117 | // Pass container padding from the first pass so the |
| 118 | // Spacing_Preprocessor can distribute it to user blocks. |
| 119 | if ( ! empty( $this->container_padding ) ) { |
| 120 | $styles['__container_padding'] = $this->container_padding; |
| 121 | } |
| 122 | } |
| 123 | $result = $this->process_manager->preprocess( $parsed_blocks, $layout, $styles, $this->get_rendering_context() ); |
| 124 | // After the first pass: find the post-content block's width and container padding. |
| 125 | if ( null === $this->post_content_width ) { |
| 126 | $this->post_content_width = $this->find_post_content_width( $result ); |
| 127 | $this->container_padding = $this->find_container_padding( $result ); |
| 128 | } |
| 129 | return $result; |
| 130 | } |
| 131 | private function find_post_content_width( array $blocks, ?array $post_content_block_names = null ): ?string { |
| 132 | if ( null === $post_content_block_names ) { |
| 133 | $post_content_block_names = (array) apply_filters( |
| 134 | 'woocommerce_email_editor_post_content_block_names', |
| 135 | array( 'core/post-content' ) |
| 136 | ); |
| 137 | } |
| 138 | foreach ( $blocks as $block ) { |
| 139 | $block_name = $block['blockName'] ?? ''; |
| 140 | if ( in_array( $block_name, $post_content_block_names, true ) ) { |
| 141 | return $block['email_attrs']['width'] ?? null; |
| 142 | } |
| 143 | if ( ! empty( $block['innerBlocks'] ) ) { |
| 144 | $found = $this->find_post_content_width( $block['innerBlocks'], $post_content_block_names ); |
| 145 | if ( null !== $found ) { |
| 146 | return $found; |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | return null; |
| 151 | } |
| 152 | private function find_container_padding( array $blocks ): array { |
| 153 | $variables_map = $this->theme_controller->get_variables_values_map(); |
| 154 | foreach ( $blocks as $block ) { |
| 155 | $email_attrs = $block['email_attrs'] ?? array(); |
| 156 | if ( ! empty( $email_attrs['suppress-horizontal-padding'] ) ) { |
| 157 | $padding = $block['attrs']['style']['spacing']['padding'] ?? array(); |
| 158 | $result = array(); |
| 159 | if ( isset( $padding['left'] ) && is_string( $padding['left'] ) ) { |
| 160 | $result['left'] = Preset_Variable_Resolver::resolve( $padding['left'], $variables_map ); |
| 161 | } |
| 162 | if ( isset( $padding['right'] ) && is_string( $padding['right'] ) ) { |
| 163 | $result['right'] = Preset_Variable_Resolver::resolve( $padding['right'], $variables_map ); |
| 164 | } |
| 165 | if ( ! empty( $result ) ) { |
| 166 | return $result; |
| 167 | } |
| 168 | } |
| 169 | if ( ! empty( $block['innerBlocks'] ) ) { |
| 170 | $found = $this->find_container_padding( $block['innerBlocks'] ); |
| 171 | if ( ! empty( $found ) ) { |
| 172 | return $found; |
| 173 | } |
| 174 | } |
| 175 | } |
| 176 | return array(); |
| 177 | } |
| 178 | public function render_block( string $block_content, array $parsed_block ): string { |
| 179 | $context = $this->get_rendering_context(); |
| 180 | $block_type = $this->block_type_registry->get_registered( $parsed_block['blockName'] ); |
| 181 | $result = null; |
| 182 | try { |
| 183 | if ( $block_type && isset( $block_type->render_email_callback ) && is_callable( $block_type->render_email_callback ) ) { |
| 184 | $result = call_user_func( $block_type->render_email_callback, $block_content, $parsed_block, $context ); |
| 185 | } |
| 186 | } catch ( \Exception $error ) { |
| 187 | $this->logger->error( |
| 188 | 'Error thrown while rendering block.', |
| 189 | array( |
| 190 | 'exception' => $error, |
| 191 | 'block_name' => $parsed_block['blockName'], |
| 192 | 'parsed_block' => $parsed_block, |
| 193 | 'message' => $error->getMessage(), |
| 194 | ) |
| 195 | ); |
| 196 | // Returning the original content. |
| 197 | return $block_content; |
| 198 | } |
| 199 | if ( null === $result ) { |
| 200 | $result = $this->fallback_renderer->render( $block_content, $parsed_block, $context ); |
| 201 | } |
| 202 | return $this->add_root_horizontal_padding( $result, $parsed_block['email_attrs'] ?? array() ); |
| 203 | } |
| 204 | private function add_root_horizontal_padding( string $content, array $email_attrs ): string { |
| 205 | $padding_left = $this->sum_padding_values( |
| 206 | $email_attrs['root-padding-left'] ?? null, |
| 207 | $email_attrs['container-padding-left'] ?? null |
| 208 | ); |
| 209 | $padding_right = $this->sum_padding_values( |
| 210 | $email_attrs['root-padding-right'] ?? null, |
| 211 | $email_attrs['container-padding-right'] ?? null |
| 212 | ); |
| 213 | $css_attrs = array(); |
| 214 | if ( $padding_left > 0 ) { |
| 215 | $css_attrs['padding-left'] = $padding_left . 'px'; |
| 216 | } |
| 217 | if ( $padding_right > 0 ) { |
| 218 | $css_attrs['padding-right'] = $padding_right . 'px'; |
| 219 | } |
| 220 | if ( empty( $css_attrs ) ) { |
| 221 | return $content; |
| 222 | } |
| 223 | $padding_style = WP_Style_Engine::compile_css( $css_attrs, '' ); |
| 224 | if ( empty( $padding_style ) ) { |
| 225 | return $content; |
| 226 | } |
| 227 | $table_attrs = array( |
| 228 | 'align' => $this->get_rendering_context()->get_default_text_align(), |
| 229 | 'width' => '100%', |
| 230 | ); |
| 231 | $cell_attrs = array( |
| 232 | 'style' => $padding_style, |
| 233 | ); |
| 234 | $div_content = sprintf( |
| 235 | '<div class="email-root-padding" style="%1$s">%2$s</div>', |
| 236 | esc_attr( $padding_style ), |
| 237 | $content |
| 238 | ); |
| 239 | return Table_Wrapper_Helper::render_outlook_table_wrapper( $div_content, $table_attrs, $cell_attrs ); |
| 240 | } |
| 241 | private function sum_padding_values( ?string $value1, ?string $value2 ): float { |
| 242 | $sum = 0.0; |
| 243 | if ( null !== $value1 ) { |
| 244 | $sum += (float) str_replace( 'px', '', $value1 ); |
| 245 | } |
| 246 | if ( null !== $value2 ) { |
| 247 | $sum += (float) str_replace( 'px', '', $value2 ); |
| 248 | } |
| 249 | return $sum; |
| 250 | } |
| 251 | private function set_template_globals( WP_Post $email_post, WP_Block_Template $template ) { |
| 252 | global $_wp_current_template_content, $_wp_current_template_id, $wp_query, $post; |
| 253 | // Backup current values of globals. |
| 254 | // Because overriding the globals can affect rendering of the page itself, we need to backup the current values. |
| 255 | $this->backup_template_content = $_wp_current_template_content; |
| 256 | $this->backup_template_id = $_wp_current_template_id; |
| 257 | $this->backup_query = $wp_query; |
| 258 | $this->backup_post = $post; |
| 259 | $_wp_current_template_id = $template->id; |
| 260 | $_wp_current_template_content = $template->content; |
| 261 | if ( $email_post->ID > 0 ) { |
| 262 | $wp_query = new \WP_Query( array( 'p' => $email_post->ID ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- We need to set the query for correct rendering the blocks. |
| 263 | } else { |
| 264 | // Synthetic post (e.g. file-template rendering): querying `p => 0` would |
| 265 | // run a real "latest posts" query, so populate an empty query manually. |
| 266 | $wp_query = new \WP_Query(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- We need to set the query for correct rendering the blocks. |
| 267 | $wp_query->post = $email_post; |
| 268 | $wp_query->posts = array( $email_post ); |
| 269 | $wp_query->post_count = 1; |
| 270 | $wp_query->found_posts = 1; |
| 271 | } |
| 272 | $post = $email_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- We need to set the post for correct rendering the blocks. |
| 273 | } |
| 274 | private function reset(): void { |
| 275 | remove_filter( 'render_block', array( $this, 'render_block' ) ); |
| 276 | remove_filter( 'block_parser_class', array( $this, 'block_parser' ) ); |
| 277 | remove_filter( 'woocommerce_email_blocks_renderer_parsed_blocks', array( $this, 'preprocess_parsed_blocks' ) ); |
| 278 | $this->post_content_width = null; |
| 279 | $this->container_padding = array(); |
| 280 | $this->rendering_context = null; |
| 281 | // Restore the original core/post-content render callback. |
| 282 | // Note: We always restore it, even if it was null originally. |
| 283 | $post_content_type = $this->block_type_registry->get_registered( 'core/post-content' ); |
| 284 | if ( $post_content_type ) { |
| 285 | // @phpstan-ignore-next-line -- WordPress core allows null for render_callback despite type definition. |
| 286 | $post_content_type->render_callback = $this->backup_post_content_callback; |
| 287 | } |
| 288 | // Restore globals to their original values. |
| 289 | global $_wp_current_template_content, $_wp_current_template_id, $wp_query, $post; |
| 290 | $_wp_current_template_content = $this->backup_template_content; |
| 291 | $_wp_current_template_id = $this->backup_template_id; |
| 292 | $wp_query = $this->backup_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Restoring of the query. |
| 293 | $post = $this->backup_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Restoring of the post. |
| 294 | } |
| 295 | private function get_rendering_context(): Rendering_Context { |
| 296 | if ( null === $this->rendering_context ) { |
| 297 | $this->rendering_context = $this->create_rendering_context(); |
| 298 | } |
| 299 | return $this->rendering_context; |
| 300 | } |
| 301 | public function create_rendering_context( ?string $language = null, ?WP_Post $post = null, ?WP_Block_Template $template = null ): Rendering_Context { |
| 302 | $email_context = apply_filters( 'woocommerce_email_editor_rendering_email_context', array(), $post, $template ); |
| 303 | if ( ! is_array( $email_context ) ) { |
| 304 | $email_context = array(); |
| 305 | } |
| 306 | return new Rendering_Context( $this->theme_controller->get_theme(), $email_context, $language ); |
| 307 | } |
| 308 | private function collect_styles( WP_Post $post, $template = null ): string { |
| 309 | $styles = (string) file_get_contents( __DIR__ . '/' . self::CONTENT_STYLES_FILE ); |
| 310 | $styles .= (string) file_get_contents( __DIR__ . '/../../content-shared.css' ); |
| 311 | // Apply default contentWidth to constrained blocks. |
| 312 | $layout = $this->theme_controller->get_layout_settings(); |
| 313 | $styles .= sprintf( |
| 314 | ' |
| 315 | .is-layout-constrained > *:not(.alignleft):not(.alignright):not(.alignfull) { |
| 316 | max-width: %1$s; |
| 317 | margin-left: auto !important; |
| 318 | margin-right: auto !important; |
| 319 | } |
| 320 | .is-layout-constrained > .alignwide { |
| 321 | max-width: %2$s; |
| 322 | margin-left: auto !important; |
| 323 | margin-right: auto !important; |
| 324 | } |
| 325 | ', |
| 326 | $layout['contentSize'], |
| 327 | $layout['wideSize'] ?? $layout['contentSize'] |
| 328 | ); |
| 329 | // Get styles from theme. |
| 330 | $styles .= $this->theme_controller->get_stylesheet_for_rendering( $post, $template ); |
| 331 | $block_support_styles = $this->theme_controller->get_stylesheet_from_context( 'block-supports', array() ); |
| 332 | // Get styles from block-supports stylesheet. This includes rules such as layout (contentWidth) that some blocks use. |
| 333 | // @see https://github.com/WordPress/WordPress/blob/3c5da9c74344aaf5bf8097f2e2c6a1a781600e03/wp-includes/script-loader.php#L3134 |
| 334 | // @internal :where is not supported by emogrifier, so we need to replace it with *. |
| 335 | $block_support_styles = str_replace( |
| 336 | ':where(:not(.alignleft):not(.alignright):not(.alignfull))', |
| 337 | '*:not(.alignleft):not(.alignright):not(.alignfull)', |
| 338 | $block_support_styles |
| 339 | ); |
| 340 | $block_support_styles = preg_replace( |
| 341 | '/group-is-layout-(\d+) >/', |
| 342 | 'group-is-layout-$1 > tbody tr td >', |
| 343 | $block_support_styles |
| 344 | ); |
| 345 | $styles .= $block_support_styles; |
| 346 | return wp_strip_all_tags( (string) apply_filters( 'woocommerce_email_content_renderer_styles', $styles, $post ) ); |
| 347 | } |
| 348 | } |
| 349 |