WooCommerce Code Reference

class-renderer.php

Source code

<?php
/**
 * This file is part of the WooCommerce Email Editor package.
 *
 * @package Automattic\WooCommerce\EmailEditor
 */

declare(strict_types = 1);
namespace Automattic\WooCommerce\EmailEditor\Engine\Renderer;

use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Content_Renderer;
use Automattic\WooCommerce\EmailEditor\Engine\Templates\Templates;
use Automattic\WooCommerce\EmailEditor\Engine\Theme_Controller;
use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
use Soundasleep\Html2Text;
use WP_Style_Engine;

/**
 * Class Renderer
 */
class Renderer {
	/**
	 * Theme controller
	 *
	 * @var Theme_Controller
	 */
	private Theme_Controller $theme_controller;

	/**
	 * Content renderer
	 *
	 * @var Content_Renderer
	 */
	private Content_Renderer $content_renderer;

	/**
	 * Templates
	 *
	 * @var Templates
	 */
	private Templates $templates;

	/**
	 * Css inliner
	 *
	 * @var Css_Inliner
	 */
	private Css_Inliner $css_inliner;

	/**
	 * Personalization tags registry
	 *
	 * @var Personalization_Tags_Registry
	 */
	private Personalization_Tags_Registry $personalization_tags_registry;

	/**
	 * Map of placeholders to full HTML comment tags for restoration.
	 *
	 * @var array
	 */
	private array $personalization_tag_placeholders = array();

	const TEMPLATE_FILE        = 'template-canvas.php';
	const TEMPLATE_STYLES_FILE = 'template-canvas.css';


	/**
	 * Renderer constructor.
	 *
	 * @param Content_Renderer              $content_renderer Content renderer.
	 * @param Templates                     $templates Templates.
	 * @param Css_Inliner                   $css_inliner CSS Inliner.
	 * @param Theme_Controller              $theme_controller Theme controller.
	 * @param Personalization_Tags_Registry $personalization_tags_registry Personalization tags registry.
	 */
	public function __construct(
		Content_Renderer $content_renderer,
		Templates $templates,
		Css_Inliner $css_inliner,
		Theme_Controller $theme_controller,
		Personalization_Tags_Registry $personalization_tags_registry
	) {
		$this->content_renderer              = $content_renderer;
		$this->templates                     = $templates;
		$this->theme_controller              = $theme_controller;
		$this->css_inliner                   = $css_inliner;
		$this->personalization_tags_registry = $personalization_tags_registry;
	}

	/**
	 * Renders the email template
	 *
	 * @param \WP_Post $post Post object.
	 * @param string   $subject Email subject.
	 * @param string   $pre_header An email preheader or preview text is the short snippet of text that follows the subject line in an inbox. See https://kb.mailpoet.com/article/418-preview-text.
	 * @param string   $language Email language.
	 * @param string   $meta_robots Optional string. Can be left empty for sending, but you can provide a value (e.g. noindex, nofollow) when you want to display email html in a browser.
	 * @param string   $template_slug Optional block template slug used for cases when email doesn't have associated template.
	 * @return array
	 */
	public function render( \WP_Post $post, string $subject, string $pre_header, string $language, string $meta_robots = '', string $template_slug = '' ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		if ( ! $template_slug ) {
			$template_slug = get_page_template_slug( $post ) ? get_page_template_slug( $post ) : 'email-general';
		}
		/** @var \WP_Block_Template $template */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
		$template = $this->templates->get_block_template( $template_slug );

		$email_styles  = $this->theme_controller->get_styles();
		$template_html = $this->content_renderer->render( $post, $template );
		$layout        = $this->theme_controller->get_layout_settings();

		ob_start();
		include self::TEMPLATE_FILE;
		$rendered_template = (string) ob_get_clean();

		$template_styles   =
		WP_Style_Engine::compile_css(
			array(
				'background-color' => $email_styles['color']['background'] ?? 'inherit',
				'color'            => $email_styles['color']['text'] ?? 'inherit',
				'padding-top'      => $email_styles['spacing']['padding']['top'] ?? '0px',
				'padding-bottom'   => $email_styles['spacing']['padding']['bottom'] ?? '0px',
				'padding-left'     => $email_styles['spacing']['padding']['left'] ?? '0px',
				'padding-right'    => $email_styles['spacing']['padding']['right'] ?? '0px',
				'font-family'      => $email_styles['typography']['fontFamily'] ?? 'inherit',
				'line-height'      => $email_styles['typography']['lineHeight'] ?? '1.5',
				'font-size'        => $email_styles['typography']['fontSize'] ?? 'inherit',
			),
			'body, .email_layout_wrapper'
		);
		$template_styles  .= '.email_layout_wrapper { box-sizing: border-box;}';
		$template_styles  .= file_get_contents( __DIR__ . '/' . self::TEMPLATE_STYLES_FILE );
		$template_styles   = '<style>' . wp_strip_all_tags( (string) apply_filters( 'woocommerce_email_renderer_styles', $template_styles, $post ) ) . '</style>';
		$rendered_template = $this->inline_css_styles( $template_styles . $rendered_template );

		// This is a workaround to support link :hover in some clients. Ideally we would remove the ability to set :hover
		// however this is not possible using the color panel from Gutenberg.
		if ( isset( $email_styles['elements']['link'][':hover']['color']['text'] ) ) {
			$rendered_template = str_replace( '<!-- Forced Styles -->', '<style>a:hover { color: ' . esc_attr( $email_styles['elements']['link'][':hover']['color']['text'] ) . ' !important; }</style>', $rendered_template );
		}

		return array(
			'html' => $rendered_template,
			'text' => $this->render_text_version( $rendered_template ),
		);
	}

	/**
	 * Inlines CSS styles into the HTML
	 *
	 * @param string $template HTML template.
	 * @return string
	 */
	private function inline_css_styles( $template ) {
		return $this->css_inliner->from_html( $template )->inline_css()->render();
	}

	/**
	 * Renders the text version of the email template.
	 *
	 * @param string $template HTML template.
	 * @return string
	 */
	private function render_text_version( $template ) {
		$template = ( mb_detect_encoding( $template, 'UTF-8', true ) ) ? $template : mb_convert_encoding( $template, 'UTF-8', mb_list_encodings() );

		// Ensure template is a string before processing.
		if ( ! is_string( $template ) ) {
			return '';
		}

		// Preserve personalization tags by temporarily replacing them with unique placeholders.
		$template = $this->preserve_personalization_tags( $template );

		$result = Html2Text::convert( (string) $template );
		if ( ! $result ) {
			return '';
		}

		// Restore personalization tags from placeholders.
		$result = $this->restore_personalization_tags( $result );

		return $result;
	}

	/**
	 * Preserves personalization tags by replacing them with unique placeholders (not inside comments).
	 *
	 * @param string $template HTML template.
	 * @return string
	 */
	private function preserve_personalization_tags( string $template ): string {
		$all_registered_tags                    = $this->personalization_tags_registry->get_all();
		$this->personalization_tag_placeholders = array();
		$counter                                = 0;

		$base_tokens    = array(); // All the tokens used in the email, e.g. [woocommerce/customer-username].
		$token_prefixes = array(); // All the used prefixes, e.g. woocommerce, mailpoet, etc.
		foreach ( $all_registered_tags as $tag ) {
			$token                 = $tag->get_token(); // E.g. [woocommerce/customer-username].
			$base_tokens[ $token ] = true;
			// Remove brackets for regex matching, escape for regex.
			$token_prefixes[] = preg_quote( substr( $token, 1, -1 ), '/' );
		}

		if ( empty( $token_prefixes ) ) {
			return $template;
		}

		// Match all of the code comments that look like a personalization tags.
		$pattern = '/<!--\[(' . implode( '|', $token_prefixes ) . ')(?:\s+[^\]]*)?\]-->/';

		$template = preg_replace_callback(
			$pattern,
			function ( $matches ) use ( &$counter, $base_tokens ) {
				// $matches[1] is the token without brackets, add brackets for lookup.
				$base_token = '[' . $matches[1] . ']';
				if ( isset( $base_tokens[ $base_token ] ) ) {
					$placeholder = 'PERSONALIZATION_TAG_PLACEHOLDER_' . $counter;
					$this->personalization_tag_placeholders[ $placeholder ] = $matches[0];
					++$counter;
					return $placeholder;
				}
				return $matches[0];
			},
			$template
		);

		return $template ?? '';
	}

	/**
	 * Restores personalization tags from placeholders
	 *
	 * @param string $text Text content.
	 * @return string
	 */
	private function restore_personalization_tags( string $text ): string {
		if ( empty( $this->personalization_tag_placeholders ) ) {
			return $text;
		}
		foreach ( $this->personalization_tag_placeholders as $placeholder => $html_comment ) {
			$text = str_replace( $placeholder, $html_comment, $text );
		}
		return $text;
	}
}