Change coding style in email editor package to WordPress

This commit contains automated changes made with phpcbf command from CodeSniffer package.
[MAILPOET-6240]
This commit is contained in:
Jan Lysý
2024-09-24 09:53:09 +02:00
committed by Jan Lysý
parent b6103b4581
commit 554adccce3
60 changed files with 2983 additions and 2853 deletions

View File

@ -5,39 +5,39 @@ namespace MailPoet\EmailEditor\Engine\Patterns\Library;
use MailPoet\EmailEditor\Utils\Cdn_Asset_Url;
abstract class Abstract_Pattern {
protected $cdnAssetUrl;
protected $blockTypes = [];
protected $templateTypes = [];
protected $inserter = true;
protected $source = 'plugin';
protected $categories = ['mailpoet'];
protected $viewportWidth = 620;
protected $cdnAssetUrl;
protected $blockTypes = array();
protected $templateTypes = array();
protected $inserter = true;
protected $source = 'plugin';
protected $categories = array( 'mailpoet' );
protected $viewportWidth = 620;
public function __construct(
Cdn_Asset_Url $cdnAssetUrl
) {
$this->cdnAssetUrl = $cdnAssetUrl;
}
public function __construct(
Cdn_Asset_Url $cdnAssetUrl
) {
$this->cdnAssetUrl = $cdnAssetUrl;
}
public function getProperties() {
return [
'title' => $this->getTitle(),
'content' => $this->getContent(),
'description' => $this->getDescription(),
'categories' => $this->categories,
'inserter' => $this->inserter,
'blockTypes' => $this->blockTypes,
'templateTypes' => $this->templateTypes,
'source' => $this->source,
'viewportWidth' => $this->viewportWidth,
];
}
public function getProperties() {
return array(
'title' => $this->getTitle(),
'content' => $this->getContent(),
'description' => $this->getDescription(),
'categories' => $this->categories,
'inserter' => $this->inserter,
'blockTypes' => $this->blockTypes,
'templateTypes' => $this->templateTypes,
'source' => $this->source,
'viewportWidth' => $this->viewportWidth,
);
}
abstract protected function getContent(): string;
abstract protected function getContent(): string;
abstract protected function getTitle(): string;
abstract protected function getTitle(): string;
protected function getDescription(): string {
return '';
}
protected function getDescription(): string {
return '';
}
}

View File

@ -3,45 +3,45 @@
namespace MailPoet\EmailEditor\Engine\Patterns\Library;
class Default_Content_Full extends Abstract_Pattern {
protected $blockTypes = [
'core/post-content',
];
protected $blockTypes = array(
'core/post-content',
);
protected $templateTypes = [
'email-general-template',
];
protected $templateTypes = array(
'email-general-template',
);
protected function getContent(): string {
return '
protected function getContent(): string {
return '
<!-- wp:columns {"backgroundColor":"white","style":{"spacing":{"padding":{"top":"var:preset|spacing|20","bottom":"var:preset|spacing|20","left":"var:preset|spacing|20","right":"var:preset|spacing|20"},"margin":{"top":"var:preset|spacing|20","bottom":"var:preset|spacing|20"}}}} -->
<div class="wp-block-columns has-white-background-color has-background" style="margin-top:var(--wp--preset--spacing--20);margin-bottom:var(--wp--preset--spacing--20);padding-top:var(--wp--preset--spacing--20);padding-right:var(--wp--preset--spacing--20);padding-bottom:var(--wp--preset--spacing--20);padding-left:var(--wp--preset--spacing--20)"><!-- wp:column -->
<div class="wp-block-column">
<!-- wp:image {"width":"130px","sizeSlug":"large"} -->
<figure class="wp-block-image size-large is-resized"><img src="' . esc_url($this->cdnAssetUrl->generateCdnUrl("email-editor/your-logo-placeholder.png")) . '" alt="Your Logo" style="width:130px"/></figure>
<figure class="wp-block-image size-large is-resized"><img src="' . esc_url( $this->cdnAssetUrl->generateCdnUrl( 'email-editor/your-logo-placeholder.png' ) ) . '" alt="Your Logo" style="width:130px"/></figure>
<!-- /wp:image -->
<!-- wp:heading {"fontSize":"medium","style":{"spacing":{"padding":{"top":"var:preset|spacing|10","bottom":"var:preset|spacing|10"}}}} -->
<h2 class="wp-block-heading has-medium-font-size" style="padding-top:var(--wp--preset--spacing--10);padding-bottom:var(--wp--preset--spacing--10)">' . __('One column layout', 'mailpoet') . '</h2>
<h2 class="wp-block-heading has-medium-font-size" style="padding-top:var(--wp--preset--spacing--10);padding-bottom:var(--wp--preset--spacing--10)">' . __( 'One column layout', 'mailpoet' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:image {"width":"620px","sizeSlug":"large"} -->
<figure class="wp-block-image"><img src="' . esc_url($this->cdnAssetUrl->generateCdnUrl("newsletter/congratulation-page-illustration-transparent-LQ.20181121-1440.png")) . '" alt="Banner Image"/></figure>
<figure class="wp-block-image"><img src="' . esc_url( $this->cdnAssetUrl->generateCdnUrl( 'newsletter/congratulation-page-illustration-transparent-LQ.20181121-1440.png' ) ) . '" alt="Banner Image"/></figure>
<!-- /wp:image -->
<!-- wp:paragraph -->
<p>' . esc_html__('A one-column layout is great for simplified and concise content, like announcements or newsletters with brief updates. Drag blocks to add content and customize your styles from the styles panel on the top right.', 'mailpoet') . '</p>
<p>' . esc_html__( 'A one-column layout is great for simplified and concise content, like announcements or newsletters with brief updates. Drag blocks to add content and customize your styles from the styles panel on the top right.', 'mailpoet' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"fontSize":"small"} -->
<p class="has-small-font-size">' . esc_html__('You received this email because you are subscribed to the [site:title]', 'mailpoet') . '</p>
<p class="has-small-font-size">' . esc_html__( 'You received this email because you are subscribed to the [site:title]', 'mailpoet' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"fontSize":"small"} -->
<p class="has-small-font-size"><a href="[link:subscription_unsubscribe_url]">' . esc_html__('Unsubscribe', 'mailpoet') . '</a> | <a href="[link:subscription_manage_url]">' . esc_html__('Manage subscription', 'mailpoet') . '</a></p>
<p class="has-small-font-size"><a href="[link:subscription_unsubscribe_url]">' . esc_html__( 'Unsubscribe', 'mailpoet' ) . '</a> | <a href="[link:subscription_manage_url]">' . esc_html__( 'Manage subscription', 'mailpoet' ) . '</a></p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
';
}
}
protected function getTitle(): string {
return __('Default Email Content with Header and Footer', 'mailpoet');
}
protected function getTitle(): string {
return __( 'Default Email Content with Header and Footer', 'mailpoet' );
}
}

View File

@ -3,36 +3,36 @@
namespace MailPoet\EmailEditor\Engine\Patterns\Library;
class Default_Content extends Abstract_Pattern {
protected $blockTypes = [
'core/post-content',
];
protected $blockTypes = array(
'core/post-content',
);
protected $templateTypes = [
'email-template',
];
protected $templateTypes = array(
'email-template',
);
protected function getContent(): string
return '
protected function getContent(): string {
return '
<!-- wp:columns {"backgroundColor":"white","style":{"spacing":{"padding":{"top":"var:preset|spacing|20","bottom":"var:preset|spacing|20","left":"var:preset|spacing|20","right":"var:preset|spacing|20"}}}} -->
<div class="wp-block-columns has-white-background-color has-background" style="padding-top:var(--wp--preset--spacing--20);padding-right:var(--wp--preset--spacing--20);padding-bottom:var(--wp--preset--spacing--20);padding-left:var(--wp--preset--spacing--20)"><!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"fontSize":"medium","style":{"spacing":{"padding":{"top":"var:preset|spacing|10","bottom":"var:preset|spacing|10"}}}} -->
<h2 class="wp-block-heading has-medium-font-size" style="padding-top:var(--wp--preset--spacing--10);padding-bottom:var(--wp--preset--spacing--10)">' . __('One column layout', 'mailpoet') . '</h2>
<h2 class="wp-block-heading has-medium-font-size" style="padding-top:var(--wp--preset--spacing--10);padding-bottom:var(--wp--preset--spacing--10)">' . __( 'One column layout', 'mailpoet' ) . '</h2>
<!-- /wp:heading -->
<!-- wp:image {"width":"620px","sizeSlug":"large"} -->
<figure class="wp-block-image"><img src="' . esc_url($this->cdnAssetUrl->generateCdnUrl("newsletter/congratulation-page-illustration-transparent-LQ.20181121-1440.png")) . '" alt="Banner Image"/></figure>
<figure class="wp-block-image"><img src="' . esc_url( $this->cdnAssetUrl->generateCdnUrl( 'newsletter/congratulation-page-illustration-transparent-LQ.20181121-1440.png' ) ) . '" alt="Banner Image"/></figure>
<!-- /wp:image -->
<!-- wp:paragraph -->
<p>' . esc_html__('A one-column layout is great for simplified and concise content, like announcements or newsletters with brief updates. Drag blocks to add content and customize your styles from the styles panel on the top right.', 'mailpoet') . '</p>
<p>' . esc_html__( 'A one-column layout is great for simplified and concise content, like announcements or newsletters with brief updates. Drag blocks to add content and customize your styles from the styles panel on the top right.', 'mailpoet' ) . '</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
';
}
}
protected function getTitle(): string {
return __('Default Email Content', 'mailpoet');
}
protected function getTitle(): string {
return __( 'Default Email Content', 'mailpoet' );
}
}

View File

@ -5,36 +5,36 @@ namespace MailPoet\EmailEditor\Engine\Patterns;
use MailPoet\EmailEditor\Utils\Cdn_Asset_Url;
class Patterns {
private $namespace = 'mailpoet';
protected $cdnAssetUrl;
private $namespace = 'mailpoet';
protected $cdnAssetUrl;
public function __construct(
Cdn_Asset_Url $cdnAssetUrl
) {
$this->cdnAssetUrl = $cdnAssetUrl;
}
public function __construct(
Cdn_Asset_Url $cdnAssetUrl
) {
$this->cdnAssetUrl = $cdnAssetUrl;
}
public function initialize(): void {
$this->registerBlockPatternCategory();
$this->registerPatterns();
}
public function initialize(): void {
$this->registerBlockPatternCategory();
$this->registerPatterns();
}
private function registerBlockPatternCategory() {
register_block_pattern_category(
'mailpoet',
[
'label' => _x('MailPoet', 'Block pattern category', 'mailpoet'),
'description' => __('A collection of email template layouts.', 'mailpoet'),
]
);
}
private function registerBlockPatternCategory() {
register_block_pattern_category(
'mailpoet',
array(
'label' => _x( 'MailPoet', 'Block pattern category', 'mailpoet' ),
'description' => __( 'A collection of email template layouts.', 'mailpoet' ),
)
);
}
private function registerPatterns() {
$this->registerPattern('default', new Library\Default_Content($this->cdnAssetUrl));
$this->registerPattern('default-full', new Library\Default_Content_Full($this->cdnAssetUrl));
}
private function registerPatterns() {
$this->registerPattern( 'default', new Library\Default_Content( $this->cdnAssetUrl ) );
$this->registerPattern( 'default-full', new Library\Default_Content_Full( $this->cdnAssetUrl ) );
}
private function registerPattern($name, $pattern) {
register_block_pattern($this->namespace . '/' . $name, $pattern->getProperties());
}
private function registerPattern( $name, $pattern ) {
register_block_pattern( $this->namespace . '/' . $name, $pattern->getProperties() );
}
}

View File

@ -8,89 +8,89 @@ use MailPoet\EmailEditor\Engine\Settings_Controller;
* This class provides functionality to render inner blocks of a block that supports reduced flex layout.
*/
class Flex_Layout_Renderer {
public function renderInnerBlocksInLayout(array $parsedBlock, Settings_Controller $settingsController): string {
$themeStyles = $settingsController->getEmailStyles();
$flexGap = $themeStyles['spacing']['blockGap'] ?? '0px';
$flexGapNumber = $settingsController->parseNumberFromStringWithPixels($flexGap);
public function renderInnerBlocksInLayout( array $parsedBlock, Settings_Controller $settingsController ): string {
$themeStyles = $settingsController->getEmailStyles();
$flexGap = $themeStyles['spacing']['blockGap'] ?? '0px';
$flexGapNumber = $settingsController->parseNumberFromStringWithPixels( $flexGap );
$marginTop = $parsedBlock['email_attrs']['margin-top'] ?? '0px';
$justify = $parsedBlock['attrs']['layout']['justifyContent'] ?? 'left';
$styles = wp_style_engine_get_styles($parsedBlock['attrs']['style'] ?? [])['css'] ?? '';
$styles .= 'margin-top: ' . $marginTop . ';';
$styles .= 'text-align: ' . $justify;
$marginTop = $parsedBlock['email_attrs']['margin-top'] ?? '0px';
$justify = $parsedBlock['attrs']['layout']['justifyContent'] ?? 'left';
$styles = wp_style_engine_get_styles( $parsedBlock['attrs']['style'] ?? array() )['css'] ?? '';
$styles .= 'margin-top: ' . $marginTop . ';';
$styles .= 'text-align: ' . $justify;
// MS Outlook doesn't support style attribute in divs so we conditionally wrap the buttons in a table and repeat styles
$outputHtml = sprintf(
'<!--[if mso | IE]><table align="%2$s" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%%"><tr><td style="%1$s" ><![endif]-->
// MS Outlook doesn't support style attribute in divs so we conditionally wrap the buttons in a table and repeat styles
$outputHtml = sprintf(
'<!--[if mso | IE]><table align="%2$s" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%%"><tr><td style="%1$s" ><![endif]-->
<div style="%1$s"><table class="layout-flex-wrapper" style="display:inline-block"><tbody><tr>',
esc_attr($styles),
esc_attr($justify)
);
esc_attr( $styles ),
esc_attr( $justify )
);
$innerBlocks = $this->computeWidthsForFlexLayout($parsedBlock, $settingsController, $flexGapNumber);
$innerBlocks = $this->computeWidthsForFlexLayout( $parsedBlock, $settingsController, $flexGapNumber );
foreach ($innerBlocks as $key => $block) {
$styles = [];
if ($block['email_attrs']['layout_width'] ?? null) {
$styles['width'] = $block['email_attrs']['layout_width'];
}
if ($key > 0) {
$styles['padding-left'] = $flexGap;
}
$outputHtml .= '<td class="layout-flex-item" style="' . esc_attr(\WP_Style_Engine::compile_css($styles, '')) . '">' . render_block($block) . '</td>';
}
$outputHtml .= '</tr></table></div>
foreach ( $innerBlocks as $key => $block ) {
$styles = array();
if ( $block['email_attrs']['layout_width'] ?? null ) {
$styles['width'] = $block['email_attrs']['layout_width'];
}
if ( $key > 0 ) {
$styles['padding-left'] = $flexGap;
}
$outputHtml .= '<td class="layout-flex-item" style="' . esc_attr( \WP_Style_Engine::compile_css( $styles, '' ) ) . '">' . render_block( $block ) . '</td>';
}
$outputHtml .= '</tr></table></div>
<!--[if mso | IE]></td></tr></table><![endif]-->';
return $outputHtml;
}
return $outputHtml;
}
private function computeWidthsForFlexLayout(array $parsedBlock, Settings_Controller $settingsController, float $flexGap): array {
// When there is no parent width we can't compute widths so auto width will be used
if (!isset($parsedBlock['email_attrs']['width'])) {
return $parsedBlock['innerBlocks'] ?? [];
}
$blocksCount = count($parsedBlock['innerBlocks']);
$totalUsedWidth = 0; // Total width assuming items without set width would consume proportional width
$parentWidth = $settingsController->parseNumberFromStringWithPixels($parsedBlock['email_attrs']['width']);
$innerBlocks = $parsedBlock['innerBlocks'] ?? [];
private function computeWidthsForFlexLayout( array $parsedBlock, Settings_Controller $settingsController, float $flexGap ): array {
// When there is no parent width we can't compute widths so auto width will be used
if ( ! isset( $parsedBlock['email_attrs']['width'] ) ) {
return $parsedBlock['innerBlocks'] ?? array();
}
$blocksCount = count( $parsedBlock['innerBlocks'] );
$totalUsedWidth = 0; // Total width assuming items without set width would consume proportional width
$parentWidth = $settingsController->parseNumberFromStringWithPixels( $parsedBlock['email_attrs']['width'] );
$innerBlocks = $parsedBlock['innerBlocks'] ?? array();
foreach ($innerBlocks as $key => $block) {
$blockWidthPercent = ($block['attrs']['width'] ?? 0) ? intval($block['attrs']['width']) : 0;
$blockWidth = floor($parentWidth * ($blockWidthPercent / 100));
// If width is not set, we assume it's 25% of the parent width
$totalUsedWidth += $blockWidth ?: floor($parentWidth * (25 / 100));
foreach ( $innerBlocks as $key => $block ) {
$blockWidthPercent = ( $block['attrs']['width'] ?? 0 ) ? intval( $block['attrs']['width'] ) : 0;
$blockWidth = floor( $parentWidth * ( $blockWidthPercent / 100 ) );
// If width is not set, we assume it's 25% of the parent width
$totalUsedWidth += $blockWidth ?: floor( $parentWidth * ( 25 / 100 ) );
if (!$blockWidth) {
$innerBlocks[$key]['email_attrs']['layout_width'] = null; // Will be rendered as auto
continue;
}
$innerBlocks[$key]['email_attrs']['layout_width'] = $this->getWidthWithoutGap($blockWidth, $flexGap, $blockWidthPercent) . 'px';
}
if ( ! $blockWidth ) {
$innerBlocks[ $key ]['email_attrs']['layout_width'] = null; // Will be rendered as auto
continue;
}
$innerBlocks[ $key ]['email_attrs']['layout_width'] = $this->getWidthWithoutGap( $blockWidth, $flexGap, $blockWidthPercent ) . 'px';
}
// When there is only one block, or percentage is set reasonably we don't need to adjust and just render as set by user
if ($blocksCount <= 1 || ($totalUsedWidth <= $parentWidth)) {
return $innerBlocks;
}
// When there is only one block, or percentage is set reasonably we don't need to adjust and just render as set by user
if ( $blocksCount <= 1 || ( $totalUsedWidth <= $parentWidth ) ) {
return $innerBlocks;
}
foreach ($innerBlocks as $key => $block) {
$proportionalSpaceOverflow = $parentWidth / $totalUsedWidth;
$blockWidth = $block['email_attrs']['layout_width'] ? $settingsController->parseNumberFromStringWithPixels($block['email_attrs']['layout_width']) : 0;
$blockProportionalWidth = $blockWidth * $proportionalSpaceOverflow;
$blockProportionalPercentage = ($blockProportionalWidth / $parentWidth) * 100;
$innerBlocks[$key]['email_attrs']['layout_width'] = $blockWidth ? $this->getWidthWithoutGap($blockProportionalWidth, $flexGap, $blockProportionalPercentage) . 'px' : null;
}
return $innerBlocks;
}
foreach ( $innerBlocks as $key => $block ) {
$proportionalSpaceOverflow = $parentWidth / $totalUsedWidth;
$blockWidth = $block['email_attrs']['layout_width'] ? $settingsController->parseNumberFromStringWithPixels( $block['email_attrs']['layout_width'] ) : 0;
$blockProportionalWidth = $blockWidth * $proportionalSpaceOverflow;
$blockProportionalPercentage = ( $blockProportionalWidth / $parentWidth ) * 100;
$innerBlocks[ $key ]['email_attrs']['layout_width'] = $blockWidth ? $this->getWidthWithoutGap( $blockProportionalWidth, $flexGap, $blockProportionalPercentage ) . 'px' : null;
}
return $innerBlocks;
}
/**
* How much of width we will strip to keep some space for the gap
* This is computed based on CSS rule used in the editor:
* For block with width set to X percent
* width: calc(X% - (var(--wp--style--block-gap) * (100 - X)/100)));
*/
private function getWidthWithoutGap(float $blockWidth, float $flexGap, float $blockWidthPercent): int {
$widthGapReduction = $flexGap * ((100 - $blockWidthPercent) / 100);
return intval(floor($blockWidth - $widthGapReduction));
}
/**
* How much of width we will strip to keep some space for the gap
* This is computed based on CSS rule used in the editor:
* For block with width set to X percent
* width: calc(X% - (var(--wp--style--block-gap) * (100 - X)/100)));
*/
private function getWidthWithoutGap( float $blockWidth, float $flexGap, float $blockWidthPercent ): int {
$widthGapReduction = $flexGap * ( ( 100 - $blockWidthPercent ) / 100 );
return intval( floor( $blockWidth - $widthGapReduction ) );
}
}

View File

@ -6,11 +6,11 @@ namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Postprocessors;
* This postprocessor replaces <mark> tags with <span> tags because mark tags are not supported across all email clients
*/
class Highlighting_Postprocessor implements Postprocessor {
public function postprocess(string $html): string {
return str_replace(
['<mark', '</mark>'],
['<span', '</span>'],
$html
);
}
public function postprocess( string $html ): string {
return str_replace(
array( '<mark', '</mark>' ),
array( '<span', '</span>' ),
$html
);
}
}

View File

@ -10,37 +10,37 @@ use MailPoet\EmailEditor\Engine\Theme_Controller;
* This postprocessor uses variables from theme.json and replaces the CSS variables with their values in final email HTML.
*/
class Variables_Postprocessor implements Postprocessor {
private Theme_Controller $themeController;
private Theme_Controller $themeController;
public function __construct(
Theme_Controller $themeController
) {
$this->themeController = $themeController;
}
public function __construct(
Theme_Controller $themeController
) {
$this->themeController = $themeController;
}
public function postprocess(string $html): string {
$variables = $this->themeController->getVariablesValuesMap();
$replacements = [];
public function postprocess( string $html ): string {
$variables = $this->themeController->getVariablesValuesMap();
$replacements = array();
foreach ($variables as $varName => $varValue) {
$varPattern = '/' . preg_quote('var(' . $varName . ')', '/') . '/i';
$replacements[$varPattern] = $varValue;
}
foreach ( $variables as $varName => $varValue ) {
$varPattern = '/' . preg_quote( 'var(' . $varName . ')', '/' ) . '/i';
$replacements[ $varPattern ] = $varValue;
}
// Pattern to match style attributes and their values.
$callback = function ($matches) use ($replacements) {
// For each match, replace CSS variables with their values
$style = $matches[1];
$style = preg_replace(array_keys($replacements), array_values($replacements), $style);
return 'style="' . esc_attr($style) . '"';
};
// Pattern to match style attributes and their values.
$callback = function ( $matches ) use ( $replacements ) {
// For each match, replace CSS variables with their values
$style = $matches[1];
$style = preg_replace( array_keys( $replacements ), array_values( $replacements ), $style );
return 'style="' . esc_attr( $style ) . '"';
};
// We want to replace the CSS variables only in the style attributes to avoid replacing the actual content.
$stylePattern = '/style="(.*?)"/i';
$stylePatternAlt = "/style='(.*?)'/i";
$html = (string)preg_replace_callback($stylePattern, $callback, $html);
$html = (string)preg_replace_callback($stylePatternAlt, $callback, $html);
// We want to replace the CSS variables only in the style attributes to avoid replacing the actual content.
$stylePattern = '/style="(.*?)"/i';
$stylePatternAlt = "/style='(.*?)'/i";
$html = (string) preg_replace_callback( $stylePattern, $callback, $html );
$html = (string) preg_replace_callback( $stylePatternAlt, $callback, $html );
return $html;
}
return $html;
}
}

View File

@ -3,5 +3,5 @@
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Postprocessors;
interface Postprocessor {
public function postprocess(string $html): string;
public function postprocess( string $html ): string;
}

View File

@ -7,98 +7,98 @@ namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
* The final width in pixels is stored in the email_attrs array because we would like to avoid changing the original attributes.
*/
class Blocks_Width_Preprocessor implements Preprocessor {
public function preprocess(array $parsedBlocks, array $layout, array $styles): array {
foreach ($parsedBlocks as $key => $block) {
// Layout width is recalculated for each block because full-width blocks don't exclude padding
$layoutWidth = $this->parseNumberFromStringWithPixels($layout['contentSize']);
$alignment = $block['attrs']['align'] ?? null;
// Subtract padding from the block width if it's not full-width
if ($alignment !== 'full') {
$layoutWidth -= $this->parseNumberFromStringWithPixels($styles['spacing']['padding']['left'] ?? '0px');
$layoutWidth -= $this->parseNumberFromStringWithPixels($styles['spacing']['padding']['right'] ?? '0px');
}
public function preprocess( array $parsedBlocks, array $layout, array $styles ): array {
foreach ( $parsedBlocks as $key => $block ) {
// Layout width is recalculated for each block because full-width blocks don't exclude padding
$layoutWidth = $this->parseNumberFromStringWithPixels( $layout['contentSize'] );
$alignment = $block['attrs']['align'] ?? null;
// Subtract padding from the block width if it's not full-width
if ( $alignment !== 'full' ) {
$layoutWidth -= $this->parseNumberFromStringWithPixels( $styles['spacing']['padding']['left'] ?? '0px' );
$layoutWidth -= $this->parseNumberFromStringWithPixels( $styles['spacing']['padding']['right'] ?? '0px' );
}
$widthInput = $block['attrs']['width'] ?? '100%';
// Currently we support only % and px units in case only the number is provided we assume it's %
// because editor saves percent values as a number.
$widthInput = is_numeric($widthInput) ? "$widthInput%" : $widthInput;
$width = $this->convertWidthToPixels($widthInput, $layoutWidth);
$widthInput = $block['attrs']['width'] ?? '100%';
// Currently we support only % and px units in case only the number is provided we assume it's %
// because editor saves percent values as a number.
$widthInput = is_numeric( $widthInput ) ? "$widthInput%" : $widthInput;
$width = $this->convertWidthToPixels( $widthInput, $layoutWidth );
if ($block['blockName'] === 'core/columns') {
// Calculate width of the columns based on the layout width and padding
$columnsWidth = $layoutWidth;
$columnsWidth -= $this->parseNumberFromStringWithPixels($block['attrs']['style']['spacing']['padding']['left'] ?? '0px');
$columnsWidth -= $this->parseNumberFromStringWithPixels($block['attrs']['style']['spacing']['padding']['right'] ?? '0px');
$borderWidth = $block['attrs']['style']['border']['width'] ?? '0px';
$columnsWidth -= $this->parseNumberFromStringWithPixels($block['attrs']['style']['border']['left']['width'] ?? $borderWidth);
$columnsWidth -= $this->parseNumberFromStringWithPixels($block['attrs']['style']['border']['right']['width'] ?? $borderWidth);
$block['innerBlocks'] = $this->addMissingColumnWidths($block['innerBlocks'], $columnsWidth);
}
if ( $block['blockName'] === 'core/columns' ) {
// Calculate width of the columns based on the layout width and padding
$columnsWidth = $layoutWidth;
$columnsWidth -= $this->parseNumberFromStringWithPixels( $block['attrs']['style']['spacing']['padding']['left'] ?? '0px' );
$columnsWidth -= $this->parseNumberFromStringWithPixels( $block['attrs']['style']['spacing']['padding']['right'] ?? '0px' );
$borderWidth = $block['attrs']['style']['border']['width'] ?? '0px';
$columnsWidth -= $this->parseNumberFromStringWithPixels( $block['attrs']['style']['border']['left']['width'] ?? $borderWidth );
$columnsWidth -= $this->parseNumberFromStringWithPixels( $block['attrs']['style']['border']['right']['width'] ?? $borderWidth );
$block['innerBlocks'] = $this->addMissingColumnWidths( $block['innerBlocks'], $columnsWidth );
}
// Copy layout styles and update width and padding
$modifiedLayout = $layout;
$modifiedLayout['contentSize'] = "{$width}px";
$modifiedStyles = $styles;
$modifiedStyles['spacing']['padding']['left'] = $block['attrs']['style']['spacing']['padding']['left'] ?? '0px';
$modifiedStyles['spacing']['padding']['right'] = $block['attrs']['style']['spacing']['padding']['right'] ?? '0px';
// Copy layout styles and update width and padding
$modifiedLayout = $layout;
$modifiedLayout['contentSize'] = "{$width}px";
$modifiedStyles = $styles;
$modifiedStyles['spacing']['padding']['left'] = $block['attrs']['style']['spacing']['padding']['left'] ?? '0px';
$modifiedStyles['spacing']['padding']['right'] = $block['attrs']['style']['spacing']['padding']['right'] ?? '0px';
$block['email_attrs']['width'] = "{$width}px";
$block['innerBlocks'] = $this->preprocess($block['innerBlocks'], $modifiedLayout, $modifiedStyles);
$parsedBlocks[$key] = $block;
}
return $parsedBlocks;
}
$block['email_attrs']['width'] = "{$width}px";
$block['innerBlocks'] = $this->preprocess( $block['innerBlocks'], $modifiedLayout, $modifiedStyles );
$parsedBlocks[ $key ] = $block;
}
return $parsedBlocks;
}
// TODO: We could add support for other units like em, rem, etc.
private function convertWidthToPixels(string $currentWidth, float $layoutWidth): float {
$width = $layoutWidth;
if (strpos($currentWidth, '%') !== false) {
$width = (float)str_replace('%', '', $currentWidth);
$width = round($width / 100 * $layoutWidth);
} elseif (strpos($currentWidth, 'px') !== false) {
$width = $this->parseNumberFromStringWithPixels($currentWidth);
}
// TODO: We could add support for other units like em, rem, etc.
private function convertWidthToPixels( string $currentWidth, float $layoutWidth ): float {
$width = $layoutWidth;
if ( strpos( $currentWidth, '%' ) !== false ) {
$width = (float) str_replace( '%', '', $currentWidth );
$width = round( $width / 100 * $layoutWidth );
} elseif ( strpos( $currentWidth, 'px' ) !== false ) {
$width = $this->parseNumberFromStringWithPixels( $currentWidth );
}
return $width;
}
return $width;
}
private function parseNumberFromStringWithPixels(string $string): float {
return (float)str_replace('px', '', $string);
}
private function parseNumberFromStringWithPixels( string $string ): float {
return (float) str_replace( 'px', '', $string );
}
private function addMissingColumnWidths(array $columns, float $columnsWidth): array {
$columnsCountWithDefinedWidth = 0;
$definedColumnWidth = 0;
$columnsCount = count($columns);
foreach ($columns as $column) {
if (isset($column['attrs']['width']) && !empty($column['attrs']['width'])) {
$columnsCountWithDefinedWidth++;
$definedColumnWidth += $this->convertWidthToPixels($column['attrs']['width'], $columnsWidth);
} else {
// When width is not set we need to add padding to the defined column width for better ratio accuracy
$definedColumnWidth += $this->parseNumberFromStringWithPixels($column['attrs']['style']['spacing']['padding']['left'] ?? '0px');
$definedColumnWidth += $this->parseNumberFromStringWithPixels($column['attrs']['style']['spacing']['padding']['right'] ?? '0px');
$borderWidth = $column['attrs']['style']['border']['width'] ?? '0px';
$definedColumnWidth += $this->parseNumberFromStringWithPixels($column['attrs']['style']['border']['left']['width'] ?? $borderWidth);
$definedColumnWidth += $this->parseNumberFromStringWithPixels($column['attrs']['style']['border']['right']['width'] ?? $borderWidth);
}
}
private function addMissingColumnWidths( array $columns, float $columnsWidth ): array {
$columnsCountWithDefinedWidth = 0;
$definedColumnWidth = 0;
$columnsCount = count( $columns );
foreach ( $columns as $column ) {
if ( isset( $column['attrs']['width'] ) && ! empty( $column['attrs']['width'] ) ) {
++$columnsCountWithDefinedWidth;
$definedColumnWidth += $this->convertWidthToPixels( $column['attrs']['width'], $columnsWidth );
} else {
// When width is not set we need to add padding to the defined column width for better ratio accuracy
$definedColumnWidth += $this->parseNumberFromStringWithPixels( $column['attrs']['style']['spacing']['padding']['left'] ?? '0px' );
$definedColumnWidth += $this->parseNumberFromStringWithPixels( $column['attrs']['style']['spacing']['padding']['right'] ?? '0px' );
$borderWidth = $column['attrs']['style']['border']['width'] ?? '0px';
$definedColumnWidth += $this->parseNumberFromStringWithPixels( $column['attrs']['style']['border']['left']['width'] ?? $borderWidth );
$definedColumnWidth += $this->parseNumberFromStringWithPixels( $column['attrs']['style']['border']['right']['width'] ?? $borderWidth );
}
}
if ($columnsCount - $columnsCountWithDefinedWidth > 0) {
$defaultColumnsWidth = round(($columnsWidth - $definedColumnWidth) / ($columnsCount - $columnsCountWithDefinedWidth), 2);
foreach ($columns as $key => $column) {
if (!isset($column['attrs']['width']) || empty($column['attrs']['width'])) {
// Add padding to the specific column width because it's not included in the default width
$columnWidth = $defaultColumnsWidth;
$columnWidth += $this->parseNumberFromStringWithPixels($column['attrs']['style']['spacing']['padding']['left'] ?? '0px');
$columnWidth += $this->parseNumberFromStringWithPixels($column['attrs']['style']['spacing']['padding']['right'] ?? '0px');
$borderWidth = $column['attrs']['style']['border']['width'] ?? '0px';
$columnWidth += $this->parseNumberFromStringWithPixels($column['attrs']['style']['border']['left']['width'] ?? $borderWidth);
$columnWidth += $this->parseNumberFromStringWithPixels($column['attrs']['style']['border']['right']['width'] ?? $borderWidth);
$columns[$key]['attrs']['width'] = "{$columnWidth}px";
}
}
}
return $columns;
}
if ( $columnsCount - $columnsCountWithDefinedWidth > 0 ) {
$defaultColumnsWidth = round( ( $columnsWidth - $definedColumnWidth ) / ( $columnsCount - $columnsCountWithDefinedWidth ), 2 );
foreach ( $columns as $key => $column ) {
if ( ! isset( $column['attrs']['width'] ) || empty( $column['attrs']['width'] ) ) {
// Add padding to the specific column width because it's not included in the default width
$columnWidth = $defaultColumnsWidth;
$columnWidth += $this->parseNumberFromStringWithPixels( $column['attrs']['style']['spacing']['padding']['left'] ?? '0px' );
$columnWidth += $this->parseNumberFromStringWithPixels( $column['attrs']['style']['spacing']['padding']['right'] ?? '0px' );
$borderWidth = $column['attrs']['style']['border']['width'] ?? '0px';
$columnWidth += $this->parseNumberFromStringWithPixels( $column['attrs']['style']['border']['left']['width'] ?? $borderWidth );
$columnWidth += $this->parseNumberFromStringWithPixels( $column['attrs']['style']['border']['right']['width'] ?? $borderWidth );
$columns[ $key ]['attrs']['width'] = "{$columnWidth}px";
}
}
}
return $columns;
}
}

View File

@ -3,15 +3,15 @@
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
class Cleanup_Preprocessor implements Preprocessor {
public function preprocess(array $parsedBlocks, array $layout, array $styles): array {
foreach ($parsedBlocks as $key => $block) {
// https://core.trac.wordpress.org/ticket/45312
// \WP_Block_Parser::parse_blocks() sometimes add a block with name null that can cause unexpected spaces in rendered content
// This behavior was reported as an issue, but it was closed as won't fix
if ($block['blockName'] === null) {
unset($parsedBlocks[$key]);
}
}
return array_values($parsedBlocks);
}
public function preprocess( array $parsedBlocks, array $layout, array $styles ): array {
foreach ( $parsedBlocks as $key => $block ) {
// https://core.trac.wordpress.org/ticket/45312
// \WP_Block_Parser::parse_blocks() sometimes add a block with name null that can cause unexpected spaces in rendered content
// This behavior was reported as an issue, but it was closed as won't fix
if ( $block['blockName'] === null ) {
unset( $parsedBlocks[ $key ] );
}
}
return array_values( $parsedBlocks );
}
}

View File

@ -7,30 +7,30 @@ namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
* In the early development phase, we are setting only margin-top for blocks that are not first or last in the columns block.
*/
class Spacing_Preprocessor implements Preprocessor {
public function preprocess(array $parsedBlocks, array $layout, array $styles): array {
$parsedBlocks = $this->addBlockGaps($parsedBlocks, $styles['spacing']['blockGap'] ?? '', null);
return $parsedBlocks;
}
public function preprocess( array $parsedBlocks, array $layout, array $styles ): array {
$parsedBlocks = $this->addBlockGaps( $parsedBlocks, $styles['spacing']['blockGap'] ?? '', null );
return $parsedBlocks;
}
private function addBlockGaps(array $parsedBlocks, string $gap = '', $parentBlock = null): array {
foreach ($parsedBlocks as $key => $block) {
$parentBlockName = $parentBlock['blockName'] ?? '';
// Ensure that email_attrs are set
$block['email_attrs'] = $block['email_attrs'] ?? [];
/**
* Do not add a gap to:
* - the top level blocks - they are post-content, and header and footer wrappers and we don't want a gap between those
* - first child
* - parent block is a buttons block (where buttons are side by side).
**/
if ($parentBlock && $key !== 0 && $gap && $parentBlockName !== 'core/buttons') {
$block['email_attrs']['margin-top'] = $gap;
}
private function addBlockGaps( array $parsedBlocks, string $gap = '', $parentBlock = null ): array {
foreach ( $parsedBlocks as $key => $block ) {
$parentBlockName = $parentBlock['blockName'] ?? '';
// Ensure that email_attrs are set
$block['email_attrs'] = $block['email_attrs'] ?? array();
/**
* Do not add a gap to:
* - the top level blocks - they are post-content, and header and footer wrappers and we don't want a gap between those
* - first child
* - parent block is a buttons block (where buttons are side by side).
*/
if ( $parentBlock && $key !== 0 && $gap && $parentBlockName !== 'core/buttons' ) {
$block['email_attrs']['margin-top'] = $gap;
}
$block['innerBlocks'] = $this->addBlockGaps($block['innerBlocks'] ?? [], $gap, $block);
$parsedBlocks[$key] = $block;
}
$block['innerBlocks'] = $this->addBlockGaps( $block['innerBlocks'] ?? array(), $gap, $block );
$parsedBlocks[ $key ] = $block;
}
return $parsedBlocks;
}
return $parsedBlocks;
}
}

View File

@ -5,82 +5,83 @@ namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class Typography_Preprocessor implements Preprocessor {
/**
* List of styles that should be copied from parent to children.
* @var string[]
*/
private const TYPOGRAPHY_STYLES = [
'color',
'font-size',
'text-decoration',
];
/**
* List of styles that should be copied from parent to children.
*
* @var string[]
*/
private const TYPOGRAPHY_STYLES = array(
'color',
'font-size',
'text-decoration',
);
/** @var Settings_Controller */
private $settingsController;
/** @var Settings_Controller */
private $settingsController;
public function __construct(
Settings_Controller $settingsController
) {
$this->settingsController = $settingsController;
}
public function __construct(
Settings_Controller $settingsController
) {
$this->settingsController = $settingsController;
}
public function preprocess(array $parsedBlocks, array $layout, array $styles): array {
foreach ($parsedBlocks as $key => $block) {
$block = $this->preprocessParent($block);
// Set defaults from theme - this needs to be done on top level blocks only
$block = $this->setDefaultsFromTheme($block);
public function preprocess( array $parsedBlocks, array $layout, array $styles ): array {
foreach ( $parsedBlocks as $key => $block ) {
$block = $this->preprocessParent( $block );
// Set defaults from theme - this needs to be done on top level blocks only
$block = $this->setDefaultsFromTheme( $block );
$block['innerBlocks'] = $this->copyTypographyFromParent($block['innerBlocks'], $block);
$parsedBlocks[$key] = $block;
}
return $parsedBlocks;
}
$block['innerBlocks'] = $this->copyTypographyFromParent( $block['innerBlocks'], $block );
$parsedBlocks[ $key ] = $block;
}
return $parsedBlocks;
}
private function copyTypographyFromParent(array $children, array $parent): array {
foreach ($children as $key => $child) {
$child = $this->preprocessParent($child);
$child['email_attrs'] = array_merge($this->filterStyles($parent['email_attrs']), $child['email_attrs']);
$child['innerBlocks'] = $this->copyTypographyFromParent($child['innerBlocks'] ?? [], $child);
$children[$key] = $child;
}
private function copyTypographyFromParent( array $children, array $parent ): array {
foreach ( $children as $key => $child ) {
$child = $this->preprocessParent( $child );
$child['email_attrs'] = array_merge( $this->filterStyles( $parent['email_attrs'] ), $child['email_attrs'] );
$child['innerBlocks'] = $this->copyTypographyFromParent( $child['innerBlocks'] ?? array(), $child );
$children[ $key ] = $child;
}
return $children;
}
return $children;
}
private function preprocessParent(array $block): array {
// Build styles that should be copied to children
$emailAttrs = [];
if (isset($block['attrs']['style']['color']['text'])) {
$emailAttrs['color'] = $block['attrs']['style']['color']['text'];
}
// In case the fontSize is set via a slug (small, medium, large, etc.) we translate it to a number
// The font size slug is set in $block['attrs']['fontSize'] and value in $block['attrs']['style']['typography']['fontSize']
if (isset($block['attrs']['fontSize'])) {
$block['attrs']['style']['typography']['fontSize'] = $this->settingsController->translateSlugToFontSize($block['attrs']['fontSize']);
}
// Pass font size to email_attrs
if (isset($block['attrs']['style']['typography']['fontSize'])) {
$emailAttrs['font-size'] = $block['attrs']['style']['typography']['fontSize'];
}
if (isset($block['attrs']['style']['typography']['textDecoration'])) {
$emailAttrs['text-decoration'] = $block['attrs']['style']['typography']['textDecoration'];
}
$block['email_attrs'] = array_merge($emailAttrs, $block['email_attrs'] ?? []);
return $block;
}
private function preprocessParent( array $block ): array {
// Build styles that should be copied to children
$emailAttrs = array();
if ( isset( $block['attrs']['style']['color']['text'] ) ) {
$emailAttrs['color'] = $block['attrs']['style']['color']['text'];
}
// In case the fontSize is set via a slug (small, medium, large, etc.) we translate it to a number
// The font size slug is set in $block['attrs']['fontSize'] and value in $block['attrs']['style']['typography']['fontSize']
if ( isset( $block['attrs']['fontSize'] ) ) {
$block['attrs']['style']['typography']['fontSize'] = $this->settingsController->translateSlugToFontSize( $block['attrs']['fontSize'] );
}
// Pass font size to email_attrs
if ( isset( $block['attrs']['style']['typography']['fontSize'] ) ) {
$emailAttrs['font-size'] = $block['attrs']['style']['typography']['fontSize'];
}
if ( isset( $block['attrs']['style']['typography']['textDecoration'] ) ) {
$emailAttrs['text-decoration'] = $block['attrs']['style']['typography']['textDecoration'];
}
$block['email_attrs'] = array_merge( $emailAttrs, $block['email_attrs'] ?? array() );
return $block;
}
private function filterStyles(array $styles): array {
return array_intersect_key($styles, array_flip(self::TYPOGRAPHY_STYLES));
}
private function filterStyles( array $styles ): array {
return array_intersect_key( $styles, array_flip( self::TYPOGRAPHY_STYLES ) );
}
private function setDefaultsFromTheme(array $block): array {
$themeData = $this->settingsController->getTheme()->get_data();
if (!($block['email_attrs']['color'] ?? '')) {
$block['email_attrs']['color'] = $themeData['styles']['color']['text'] ?? null;
}
if (!($block['email_attrs']['font-size'] ?? '')) {
$block['email_attrs']['font-size'] = $themeData['styles']['typography']['fontSize'];
}
return $block;
}
private function setDefaultsFromTheme( array $block ): array {
$themeData = $this->settingsController->getTheme()->get_data();
if ( ! ( $block['email_attrs']['color'] ?? '' ) ) {
$block['email_attrs']['color'] = $themeData['styles']['color']['text'] ?? null;
}
if ( ! ( $block['email_attrs']['font-size'] ?? '' ) ) {
$block['email_attrs']['font-size'] = $themeData['styles']['typography']['fontSize'];
}
return $block;
}
}

View File

@ -3,9 +3,9 @@
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
interface Preprocessor {
/**
* @param array{contentSize: string} $layout
* @param array{spacing: array{padding: array{bottom: string, left: string, right: string, top: string}, blockGap: string}} $styles
*/
public function preprocess(array $parsedBlocks, array $layout, array $styles): array;
/**
* @param array{contentSize: string} $layout
* @param array{spacing: array{padding: array{bottom: string, left: string, right: string, top: string}, blockGap: string}} $styles
*/
public function preprocess( array $parsedBlocks, array $layout, array $styles ): array;
}

View File

@ -5,5 +5,5 @@ namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer;
use MailPoet\EmailEditor\Engine\Settings_Controller;
interface Block_Renderer {
public function render(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string;
public function render( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string;
}

View File

@ -5,15 +5,15 @@ namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer;
use WP_Block_Parser;
class Blocks_Parser extends WP_Block_Parser {
/**
* List of parsed blocks
*
* @var \WP_Block_Parser_Block[]
*/
public $output;
/**
* List of parsed blocks
*
* @var \WP_Block_Parser_Block[]
*/
public $output;
public function parse($document) {
parent::parse($document);
return apply_filters('mailpoet_blocks_renderer_parsed_blocks', $this->output);
}
public function parse( $document ) {
parent::parse( $document );
return apply_filters( 'mailpoet_blocks_renderer_parsed_blocks', $this->output );
}
}

View File

@ -4,38 +4,38 @@ namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer;
class Blocks_Registry {
/** @var Block_Renderer[] */
private $blockRenderersMap = [];
/** @var BlockRenderer */
private $fallbackRenderer = null;
/** @var Block_Renderer[] */
private $blockRenderersMap = array();
/** @var BlockRenderer */
private $fallbackRenderer = null;
public function addBlockRenderer(string $blockName, Block_Renderer $renderer): void {
$this->blockRenderersMap[$blockName] = $renderer;
}
public function addBlockRenderer( string $blockName, Block_Renderer $renderer ): void {
$this->blockRenderersMap[ $blockName ] = $renderer;
}
public function addFallbackRenderer(BlockRenderer $renderer): void {
$this->fallbackRenderer = $renderer;
}
public function addFallbackRenderer( BlockRenderer $renderer ): void {
$this->fallbackRenderer = $renderer;
}
public function hasBlockRenderer(string $blockName): bool {
return isset($this->blockRenderersMap[$blockName]);
}
public function hasBlockRenderer( string $blockName ): bool {
return isset( $this->blockRenderersMap[ $blockName ] );
}
public function getBlockRenderer(string $blockName): ?Block_Renderer {
return $this->blockRenderersMap[$blockName] ?? null;
}
public function getBlockRenderer( string $blockName ): ?Block_Renderer {
return $this->blockRenderersMap[ $blockName ] ?? null;
}
public function getFallbackRenderer(): ?BlockRenderer {
return $this->fallbackRenderer;
}
public function getFallbackRenderer(): ?BlockRenderer {
return $this->fallbackRenderer;
}
public function removeAllBlockRenderers(): void {
foreach (array_keys($this->blockRenderersMap) as $blockName) {
$this->removeBlockRenderer($blockName);
}
}
public function removeAllBlockRenderers(): void {
foreach ( array_keys( $this->blockRenderersMap ) as $blockName ) {
$this->removeBlockRenderer( $blockName );
}
}
private function removeBlockRenderer(string $blockName): void {
unset($this->blockRenderersMap[$blockName]);
}
private function removeBlockRenderer( string $blockName ): void {
unset( $this->blockRenderersMap[ $blockName ] );
}
}

View File

@ -9,92 +9,92 @@ use WP_Block_Template;
use WP_Post;
class Content_Renderer {
private Blocks_Registry $blocksRegistry;
private Process_Manager $processManager;
private Settings_Controller $settingsController;
private Theme_Controller $themeController;
private $post = null;
private $template = null;
private Blocks_Registry $blocksRegistry;
private Process_Manager $processManager;
private Settings_Controller $settingsController;
private Theme_Controller $themeController;
private $post = null;
private $template = null;
const CONTENT_STYLES_FILE = 'content.css';
const CONTENT_STYLES_FILE = 'content.css';
public function __construct(
Process_Manager $preprocessManager,
Blocks_Registry $blocksRegistry,
Settings_Controller $settingsController,
Theme_Controller $themeController
) {
$this->processManager = $preprocessManager;
$this->blocksRegistry = $blocksRegistry;
$this->settingsController = $settingsController;
$this->themeController = $themeController;
}
public function __construct(
Process_Manager $preprocessManager,
Blocks_Registry $blocksRegistry,
Settings_Controller $settingsController,
Theme_Controller $themeController
) {
$this->processManager = $preprocessManager;
$this->blocksRegistry = $blocksRegistry;
$this->settingsController = $settingsController;
$this->themeController = $themeController;
}
private function initialize() {
add_filter('render_block', [$this, 'renderBlock'], 10, 2);
add_filter('block_parser_class', [$this, 'blockParser']);
add_filter('mailpoet_blocks_renderer_parsed_blocks', [$this, 'preprocessParsedBlocks']);
private function initialize() {
add_filter( 'render_block', array( $this, 'renderBlock' ), 10, 2 );
add_filter( 'block_parser_class', array( $this, 'blockParser' ) );
add_filter( 'mailpoet_blocks_renderer_parsed_blocks', array( $this, 'preprocessParsedBlocks' ) );
do_action('mailpoet_blocks_renderer_initialized', $this->blocksRegistry);
}
do_action( 'mailpoet_blocks_renderer_initialized', $this->blocksRegistry );
}
public function render(WP_Post $post, WP_Block_Template $template): string {
$this->post = $post;
$this->template = $template;
$this->setTemplateGlobals($post, $template);
$this->initialize();
$renderedHtml = get_the_block_template_html();
$this->reset();
public function render( WP_Post $post, WP_Block_Template $template ): string {
$this->post = $post;
$this->template = $template;
$this->setTemplateGlobals( $post, $template );
$this->initialize();
$renderedHtml = get_the_block_template_html();
$this->reset();
return $this->processManager->postprocess($this->inlineStyles($renderedHtml, $post, $template));
}
return $this->processManager->postprocess( $this->inlineStyles( $renderedHtml, $post, $template ) );
}
public function blockParser() {
return 'MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Blocks_Parser';
}
public function blockParser() {
return 'MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Blocks_Parser';
}
public function preprocessParsedBlocks(array $parsedBlocks): array {
return $this->processManager->preprocess($parsedBlocks, $this->themeController->getLayoutSettings(), $this->themeController->getStyles($this->post, $this->template));
}
public function preprocessParsedBlocks( array $parsedBlocks ): array {
return $this->processManager->preprocess( $parsedBlocks, $this->themeController->getLayoutSettings(), $this->themeController->getStyles( $this->post, $this->template ) );
}
public function renderBlock($blockContent, $parsedBlock) {
$renderer = $this->blocksRegistry->getBlockRenderer($parsedBlock['blockName']);
if (!$renderer) {
$renderer = $this->blocksRegistry->getFallbackRenderer();
}
return $renderer ? $renderer->render($blockContent, $parsedBlock, $this->settingsController) : $blockContent;
}
public function renderBlock( $blockContent, $parsedBlock ) {
$renderer = $this->blocksRegistry->getBlockRenderer( $parsedBlock['blockName'] );
if ( ! $renderer ) {
$renderer = $this->blocksRegistry->getFallbackRenderer();
}
return $renderer ? $renderer->render( $blockContent, $parsedBlock, $this->settingsController ) : $blockContent;
}
private function setTemplateGlobals(WP_Post $post, WP_Block_Template $template) {
global $_wp_current_template_content, $_wp_current_template_id;
$_wp_current_template_id = $template->id;
$_wp_current_template_content = $template->content;
$GLOBALS['post'] = $post;
}
private function setTemplateGlobals( WP_Post $post, WP_Block_Template $template ) {
global $_wp_current_template_content, $_wp_current_template_id;
$_wp_current_template_id = $template->id;
$_wp_current_template_content = $template->content;
$GLOBALS['post'] = $post;
}
/**
* As we use default WordPress filters, we need to remove them after email rendering
* so that we don't interfere with possible post rendering that might happen later.
*/
private function reset() {
$this->blocksRegistry->removeAllBlockRenderers();
remove_filter('render_block', [$this, 'renderBlock']);
remove_filter('block_parser_class', [$this, 'blockParser']);
remove_filter('mailpoet_blocks_renderer_parsed_blocks', [$this, 'preprocessParsedBlocks']);
}
/**
* As we use default WordPress filters, we need to remove them after email rendering
* so that we don't interfere with possible post rendering that might happen later.
*/
private function reset() {
$this->blocksRegistry->removeAllBlockRenderers();
remove_filter( 'render_block', array( $this, 'renderBlock' ) );
remove_filter( 'block_parser_class', array( $this, 'blockParser' ) );
remove_filter( 'mailpoet_blocks_renderer_parsed_blocks', array( $this, 'preprocessParsedBlocks' ) );
}
/**
* @param string $html
* @return string
*/
private function inlineStyles($html, WP_Post $post, $template = null) {
$styles = (string)file_get_contents(dirname(__FILE__) . '/' . self::CONTENT_STYLES_FILE);
$styles .= (string)file_get_contents(dirname(__FILE__) . '/../../content-shared.css');
/**
* @param string $html
* @return string
*/
private function inlineStyles( $html, WP_Post $post, $template = null ) {
$styles = (string) file_get_contents( __DIR__ . '/' . self::CONTENT_STYLES_FILE );
$styles .= (string) file_get_contents( __DIR__ . '/../../content-shared.css' );
// Apply default contentWidth to constrained blocks.
$layout = $this->themeController->getLayoutSettings();
$styles .= sprintf(
'
// Apply default contentWidth to constrained blocks.
$layout = $this->themeController->getLayoutSettings();
$styles .= sprintf(
'
.is-layout-constrained > *:not(.alignleft):not(.alignright):not(.alignfull) {
max-width: %1$s;
margin-left: auto !important;
@ -106,40 +106,40 @@ class Content_Renderer {
margin-right: auto !important;
}
',
$layout['contentSize'],
$layout['wideSize']
);
$layout['contentSize'],
$layout['wideSize']
);
// Get styles from theme.
$styles .= $this->themeController->getStylesheetForRendering($post, $template);
$blockSupportStyles = $this->themeController->getStylesheetFromContext('block-supports', []);
// Get styles from block-supports stylesheet. This includes rules such as layout (contentWidth) that some blocks use.
// @see https://github.com/WordPress/WordPress/blob/3c5da9c74344aaf5bf8097f2e2c6a1a781600e03/wp-includes/script-loader.php#L3134
// @internal :where is not supported by emogrifier, so we need to replace it with *.
$blockSupportStyles = str_replace(
':where(:not(.alignleft):not(.alignright):not(.alignfull))',
'*:not(.alignleft):not(.alignright):not(.alignfull)',
$blockSupportStyles
);
// Layout CSS assumes the top level block will have a single DIV wrapper with children. Since our blocks use tables,
// we need to adjust this to look for children in the TD element. This may requires more advanced replacement but
// this works in the current version of Gutenberg.
// Example rule we're targetting: .wp-container-core-group-is-layout-1.wp-container-core-group-is-layout-1 > *
$blockSupportStyles = preg_replace(
'/group-is-layout-(\d+) >/',
'group-is-layout-$1 > tbody tr td >',
$blockSupportStyles
);
// Get styles from theme.
$styles .= $this->themeController->getStylesheetForRendering( $post, $template );
$blockSupportStyles = $this->themeController->getStylesheetFromContext( 'block-supports', array() );
// Get styles from block-supports stylesheet. This includes rules such as layout (contentWidth) that some blocks use.
// @see https://github.com/WordPress/WordPress/blob/3c5da9c74344aaf5bf8097f2e2c6a1a781600e03/wp-includes/script-loader.php#L3134
// @internal :where is not supported by emogrifier, so we need to replace it with *.
$blockSupportStyles = str_replace(
':where(:not(.alignleft):not(.alignright):not(.alignfull))',
'*:not(.alignleft):not(.alignright):not(.alignfull)',
$blockSupportStyles
);
// Layout CSS assumes the top level block will have a single DIV wrapper with children. Since our blocks use tables,
// we need to adjust this to look for children in the TD element. This may requires more advanced replacement but
// this works in the current version of Gutenberg.
// Example rule we're targetting: .wp-container-core-group-is-layout-1.wp-container-core-group-is-layout-1 > *
$blockSupportStyles = preg_replace(
'/group-is-layout-(\d+) >/',
'group-is-layout-$1 > tbody tr td >',
$blockSupportStyles
);
$styles .= $blockSupportStyles;
$styles .= $blockSupportStyles;
// Debugging for content styles. Remember these get inlined.
//echo '<pre>';
//var_dump($styles);
//echo '</pre>';
// Debugging for content styles. Remember these get inlined.
// echo '<pre>';
// var_dump($styles);
// echo '</pre>';
$styles = '<style>' . wp_strip_all_tags((string)apply_filters('mailpoet_email_content_renderer_styles', $styles, $post)) . '</style>';
$styles = '<style>' . wp_strip_all_tags( (string) apply_filters( 'mailpoet_email_content_renderer_styles', $styles, $post ) ) . '</style>';
return CssInliner::fromHtml($styles . $html)->inlineCss()->render();
}
return CssInliner::fromHtml( $styles . $html )->inlineCss()->render();
}
}

View File

@ -12,53 +12,53 @@ use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors\Spacing_P
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors\Typography_Preprocessor;
class Process_Manager {
/** @var Preprocessor[] */
private $preprocessors = [];
/** @var Preprocessor[] */
private $preprocessors = array();
/** @var Postprocessor[] */
private $postprocessors = [];
/** @var Postprocessor[] */
private $postprocessors = array();
public function __construct(
Cleanup_Preprocessor $cleanupPreprocessor,
Blocks_Width_Preprocessor $blocksWidthPreprocessor,
Typography_Preprocessor $typographyPreprocessor,
Spacing_Preprocessor $spacingPreprocessor,
Highlighting_Postprocessor $highlightingPostprocessor,
Variables_Postprocessor $variablesPostprocessor
) {
$this->registerPreprocessor($cleanupPreprocessor);
$this->registerPreprocessor($blocksWidthPreprocessor);
$this->registerPreprocessor($typographyPreprocessor);
$this->registerPreprocessor($spacingPreprocessor);
$this->registerPostprocessor($highlightingPostprocessor);
$this->registerPostprocessor($variablesPostprocessor);
}
public function __construct(
Cleanup_Preprocessor $cleanupPreprocessor,
Blocks_Width_Preprocessor $blocksWidthPreprocessor,
Typography_Preprocessor $typographyPreprocessor,
Spacing_Preprocessor $spacingPreprocessor,
Highlighting_Postprocessor $highlightingPostprocessor,
Variables_Postprocessor $variablesPostprocessor
) {
$this->registerPreprocessor( $cleanupPreprocessor );
$this->registerPreprocessor( $blocksWidthPreprocessor );
$this->registerPreprocessor( $typographyPreprocessor );
$this->registerPreprocessor( $spacingPreprocessor );
$this->registerPostprocessor( $highlightingPostprocessor );
$this->registerPostprocessor( $variablesPostprocessor );
}
/**
* @param array $parsedBlocks
* @param array{contentSize: string} $layout
* @param array{spacing: array{padding: array{bottom: string, left: string, right: string, top: string}, blockGap: string}} $styles
* @return array
*/
public function preprocess(array $parsedBlocks, array $layout, array $styles): array {
foreach ($this->preprocessors as $preprocessor) {
$parsedBlocks = $preprocessor->preprocess($parsedBlocks, $layout, $styles);
}
return $parsedBlocks;
}
/**
* @param array $parsedBlocks
* @param array{contentSize: string} $layout
* @param array{spacing: array{padding: array{bottom: string, left: string, right: string, top: string}, blockGap: string}} $styles
* @return array
*/
public function preprocess( array $parsedBlocks, array $layout, array $styles ): array {
foreach ( $this->preprocessors as $preprocessor ) {
$parsedBlocks = $preprocessor->preprocess( $parsedBlocks, $layout, $styles );
}
return $parsedBlocks;
}
public function postprocess(string $html): string {
foreach ($this->postprocessors as $postprocessor) {
$html = $postprocessor->postprocess($html);
}
return $html;
}
public function postprocess( string $html ): string {
foreach ( $this->postprocessors as $postprocessor ) {
$html = $postprocessor->postprocess( $html );
}
return $html;
}
public function registerPreprocessor(Preprocessor $preprocessor): void {
$this->preprocessors[] = $preprocessor;
}
public function registerPreprocessor( Preprocessor $preprocessor ): void {
$this->preprocessors[] = $preprocessor;
}
public function registerPostprocessor(Postprocessor $postprocessor): void {
$this->postprocessors[] = $postprocessor;
}
public function registerPostprocessor( Postprocessor $postprocessor ): void {
$this->postprocessors[] = $postprocessor;
}
}

View File

@ -1,28 +1,28 @@
/**
CSS reset for email clients for elements used in email content
StyleLint is disabled because some rules contain properties that linter marks as unknown (e.g. mso- prefix), but they are valid for email rendering
CSS reset for email clients for elements used in email content
StyleLint is disabled because some rules contain properties that linter marks as unknown (e.g. mso- prefix), but they are valid for email rendering
*/
/* stylelint-disable property-no-unknown */
table,
td {
border-collapse: collapse;
mso-table-lspace: 0;
mso-table-rspace: 0;
border-collapse: collapse;
mso-table-lspace: 0;
mso-table-rspace: 0;
}
img {
border: 0;
height: auto;
-ms-interpolation-mode: bicubic;
line-height: 100%;
max-width: 100%;
outline: none;
text-decoration: none;
border: 0;
height: auto;
-ms-interpolation-mode: bicubic;
line-height: 100%;
max-width: 100%;
outline: none;
text-decoration: none;
}
p {
display: block;
margin: 0;
display: block;
margin: 0;
}
h1,
@ -31,25 +31,25 @@ h3,
h4,
h5,
h6 {
margin-bottom: 0;
margin-top: 0;
margin-bottom: 0;
margin-top: 0;
}
/* Ensure border style is set when a block has a border */
.has-border-color {
border-style: solid;
border-style: solid;
}
/* We want ensure the same design for all email clients */
ul,
ol {
/* When margin attribute is set to zero, Outlook doesn't render the list properly. As a possible workaround, we can reset only margin for top and bottom */
margin-bottom: 0;
margin-top: 0;
padding: 0 0 0 40px;
/* When margin attribute is set to zero, Outlook doesn't render the list properly. As a possible workaround, we can reset only margin for top and bottom */
margin-bottom: 0;
margin-top: 0;
padding: 0 0 0 40px;
}
/* Outlook was adding weird spaces around lists in some versions. Resetting vertical margin for list items solved it */
li {
margin-bottom: 0;
margin-top: 0;
margin-bottom: 0;
margin-top: 0;
}

View File

@ -11,94 +11,94 @@ use WP_Style_Engine;
use WP_Theme_JSON;
class Renderer {
private Theme_Controller $themeController;
private Content_Renderer $contentRenderer;
private Templates $templates;
/** @var WP_Theme_JSON|null */
private static $theme = null;
private Theme_Controller $themeController;
private Content_Renderer $contentRenderer;
private Templates $templates;
/** @var WP_Theme_JSON|null */
private static $theme = null;
const TEMPLATE_FILE = 'template-canvas.php';
const TEMPLATE_STYLES_FILE = 'template-canvas.css';
const TEMPLATE_FILE = 'template-canvas.php';
const TEMPLATE_STYLES_FILE = 'template-canvas.css';
public function __construct(
Content_Renderer $contentRenderer,
Templates $templates,
Theme_Controller $themeController
) {
$this->contentRenderer = $contentRenderer;
$this->templates = $templates;
$this->themeController = $themeController;
}
public function __construct(
Content_Renderer $contentRenderer,
Templates $templates,
Theme_Controller $themeController
) {
$this->contentRenderer = $contentRenderer;
$this->templates = $templates;
$this->themeController = $themeController;
}
/**
* During rendering, this stores the theme data for the template being rendered.
*/
public static function getTheme() {
return self::$theme;
}
/**
* During rendering, this stores the theme data for the template being rendered.
*/
public static function getTheme() {
return self::$theme;
}
public function render(\WP_Post $post, string $subject, string $preHeader, string $language, $metaRobots = ''): array {
$templateId = 'mailpoet/mailpoet//' . (get_page_template_slug($post) ?: 'email-general');
$template = $this->templates->getBlockTemplate($templateId);
$theme = $this->templates->getBlockTemplateTheme($templateId, $template->wp_id); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
public function render( \WP_Post $post, string $subject, string $preHeader, string $language, $metaRobots = '' ): array {
$templateId = 'mailpoet/mailpoet//' . ( get_page_template_slug( $post ) ?: 'email-general' );
$template = $this->templates->getBlockTemplate( $templateId );
$theme = $this->templates->getBlockTemplateTheme( $templateId, $template->wp_id ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
// Set the theme for the template. This is merged with base theme.json and core json before rendering.
self::$theme = new WP_Theme_JSON($theme, 'default');
// Set the theme for the template. This is merged with base theme.json and core json before rendering.
self::$theme = new WP_Theme_JSON( $theme, 'default' );
$emailStyles = $this->themeController->getStyles($post, $template);
$templateHtml = $this->contentRenderer->render($post, $template);
$layout = $this->themeController->getLayoutSettings();
$emailStyles = $this->themeController->getStyles( $post, $template );
$templateHtml = $this->contentRenderer->render( $post, $template );
$layout = $this->themeController->getLayoutSettings();
ob_start();
include self::TEMPLATE_FILE;
$renderedTemplate = (string)ob_get_clean();
ob_start();
include self::TEMPLATE_FILE;
$renderedTemplate = (string) ob_get_clean();
$templateStyles =
WP_Style_Engine::compile_css(
[
'background-color' => $emailStyles['color']['background'] ?? 'inherit',
'color' => $emailStyles['color']['text'] ?? 'inherit',
'padding-top' => $emailStyles['spacing']['padding']['top'] ?? '0px',
'padding-bottom' => $emailStyles['spacing']['padding']['bottom'] ?? '0px',
'padding-left' => $emailStyles['spacing']['padding']['left'] ?? '0px',
'padding-right' => $emailStyles['spacing']['padding']['right'] ?? '0px',
'font-family' => $emailStyles['typography']['fontFamily'] ?? 'inherit',
'line-height' => $emailStyles['typography']['lineHeight'] ?? '1.5',
'font-size' => $emailStyles['typography']['fontSize'] ?? 'inherit',
],
'body, .email_layout_wrapper'
);
$templateStyles .= '.email_layout_wrapper { box-sizing: border-box;}';
$templateStyles .= file_get_contents(dirname(__FILE__) . '/' . self::TEMPLATE_STYLES_FILE);
$templateStyles = '<style>' . wp_strip_all_tags((string)apply_filters('mailpoet_email_renderer_styles', $templateStyles, $post)) . '</style>';
$renderedTemplate = $this->inlineCSSStyles($templateStyles . $renderedTemplate);
$templateStyles =
WP_Style_Engine::compile_css(
array(
'background-color' => $emailStyles['color']['background'] ?? 'inherit',
'color' => $emailStyles['color']['text'] ?? 'inherit',
'padding-top' => $emailStyles['spacing']['padding']['top'] ?? '0px',
'padding-bottom' => $emailStyles['spacing']['padding']['bottom'] ?? '0px',
'padding-left' => $emailStyles['spacing']['padding']['left'] ?? '0px',
'padding-right' => $emailStyles['spacing']['padding']['right'] ?? '0px',
'font-family' => $emailStyles['typography']['fontFamily'] ?? 'inherit',
'line-height' => $emailStyles['typography']['lineHeight'] ?? '1.5',
'font-size' => $emailStyles['typography']['fontSize'] ?? 'inherit',
),
'body, .email_layout_wrapper'
);
$templateStyles .= '.email_layout_wrapper { box-sizing: border-box;}';
$templateStyles .= file_get_contents( __DIR__ . '/' . self::TEMPLATE_STYLES_FILE );
$templateStyles = '<style>' . wp_strip_all_tags( (string) apply_filters( 'mailpoet_email_renderer_styles', $templateStyles, $post ) ) . '</style>';
$renderedTemplate = $this->inlineCSSStyles( $templateStyles . $renderedTemplate );
// 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($emailStyles['elements']['link'][':hover']['color']['text'])) {
$renderedTemplate = str_replace('<!-- Forced Styles -->', '<style>a:hover { color: ' . esc_attr($emailStyles['elements']['link'][':hover']['color']['text']) . ' !important; }</style>', $renderedTemplate);
}
// 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( $emailStyles['elements']['link'][':hover']['color']['text'] ) ) {
$renderedTemplate = str_replace( '<!-- Forced Styles -->', '<style>a:hover { color: ' . esc_attr( $emailStyles['elements']['link'][':hover']['color']['text'] ) . ' !important; }</style>', $renderedTemplate );
}
return [
'html' => $renderedTemplate,
'text' => $this->renderTextVersion($renderedTemplate),
];
}
return array(
'html' => $renderedTemplate,
'text' => $this->renderTextVersion( $renderedTemplate ),
);
}
/**
* @param string $template
* @return string
*/
private function inlineCSSStyles($template) {
return CssInliner::fromHtml($template)->inlineCss()->render();
}
/**
* @param string $template
* @return string
*/
private function inlineCSSStyles( $template ) {
return CssInliner::fromHtml( $template )->inlineCss()->render();
}
/**
* @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 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 );
}
}

View File

@ -3,89 +3,89 @@
/* StyleLint is disabled because some rules contain properties that linter marks as unknown (e.g. mso- prefix), 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% */
word-spacing: normal;
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% */
word-spacing: normal;
}
a {
text-decoration: none;
text-decoration: none;
}
.email_layout_wrapper {
margin: 0 auto;
width: 100%;
margin: 0 auto;
width: 100%;
}
.email_content_wrapper {
direction: ltr;
font-size: inherit;
text-align: left;
direction: ltr;
font-size: inherit;
text-align: left;
}
.email_footer {
direction: ltr;
text-align: center;
direction: ltr;
text-align: center;
}
/* 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;
-webkit-text-size-adjust: none;
visibility: hidden;
color: #fff;
display: none;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
mso-hide: all;
opacity: 0;
overflow: hidden;
-webkit-text-size-adjust: none;
visibility: hidden;
}
@media screen and (max-width: 660px) {
.email-block-column-content {
max-width: 100% !important;
}
.block {
display: block;
width: 100% !important;
}
.email-block-column-content {
max-width: 100% !important;
}
.block {
display: block;
width: 100% !important;
}
/* Ensure proper width of columns on mobile when we set 100% and a border is set */
.email-block-column {
box-sizing: border-box;
}
/* Ensure proper width of columns on mobile when we set 100% and a border is set */
.email-block-column {
box-sizing: border-box;
}
/* We set width to some tables e.g. for wrappers of horizontally aligned images and we force width 100% on mobile */
.email-table-with-width {
width: 100% !important;
}
/* We set width to some tables e.g. for wrappers of horizontally aligned images and we force width 100% on mobile */
.email-table-with-width {
width: 100% !important;
}
/* Flex Layout */
.layout-flex-wrapper,
.layout-flex-wrapper tbody,
.layout-flex-wrapper tr {
display: block !important;
width: 100% !important;
}
/* Flex Layout */
.layout-flex-wrapper,
.layout-flex-wrapper tbody,
.layout-flex-wrapper tr {
display: block !important;
width: 100% !important;
}
.layout-flex-item {
display: block !important;
padding-bottom: 8px !important; /* Half of the flex gap between blocks */
padding-left: 0 !important;
width: 100% !important;
}
.layout-flex-item {
display: block !important;
padding-bottom: 8px !important; /* Half of the flex gap between blocks */
padding-left: 0 !important;
width: 100% !important;
}
.layout-flex-item table,
.layout-flex-item td {
box-sizing: border-box !important;
display: block !important;
width: 100% !important;
}
/* Flex Layout End */
.layout-flex-item table,
.layout-flex-item td {
box-sizing: border-box !important;
display: block !important;
width: 100% !important;
}
/* Flex Layout End */
}
/* stylelint-enable property-no-unknown */

View File

@ -6,6 +6,7 @@
* Template file to render the current 'wp_template', specifcally for emails.
*
* Variables passed to this template:
*
* @var $subject string
* @var $preHeader string
* @var $templateHtml string
@ -15,33 +16,33 @@
?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<title><?php echo esc_html($subject); ?></title>
<meta charset="<?php bloginfo('charset'); ?>" />
<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" />
<?php echo $metaRobots; // HTML defined by MailPoet--do not escape ?>
<!-- Forced Styles -->
<title><?php echo esc_html( $subject ); ?></title>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<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" />
<?php echo $metaRobots; // HTML defined by MailPoet--do not escape ?>
<!-- Forced Styles -->
</head>
<body>
<!--[if mso | IE]><table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" width="<?php echo esc_attr($layout['contentSize']); ?>" style="width:<?php echo esc_attr($layout['contentSize']); ?>"><tr><td><![endif]-->
<div class="email_layout_wrapper" style="max-width: <?php echo esc_attr($layout['contentSize']); ?>">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td class="email_preheader" height="1">
<?php echo esc_html(wp_strip_all_tags($preHeader)); ?>
</td>
</tr>
<tr>
<td class="email_content_wrapper">
<?php echo $templateHtml; ?>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!--[if mso | IE]><table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" width="<?php echo esc_attr( $layout['contentSize'] ); ?>" style="width:<?php echo esc_attr( $layout['contentSize'] ); ?>"><tr><td><![endif]-->
<div class="email_layout_wrapper" style="max-width: <?php echo esc_attr( $layout['contentSize'] ); ?>">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td class="email_preheader" height="1">
<?php echo esc_html( wp_strip_all_tags( $preHeader ) ); ?>
</td>
</tr>
<tr>
<td class="email_content_wrapper">
<?php echo $templateHtml; ?>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</body>
</html>

View File

@ -8,50 +8,50 @@ use MailPoet\EmailEditor\Validator\Builder;
use WP_Theme_JSON;
class Template_Preview {
private Theme_Controller $themeController;
private Settings_Controller $settingsController;
private Templates $templates;
private Theme_Controller $themeController;
private Settings_Controller $settingsController;
private Templates $templates;
public function __construct(
Theme_Controller $themeController,
Settings_Controller $settingsController,
Templates $templates
) {
$this->themeController = $themeController;
$this->settingsController = $settingsController;
$this->templates = $templates;
}
public function __construct(
Theme_Controller $themeController,
Settings_Controller $settingsController,
Templates $templates
) {
$this->themeController = $themeController;
$this->settingsController = $settingsController;
$this->templates = $templates;
}
public function initialize(): void {
register_rest_field(
'wp_template',
'email_theme_css',
[
'get_callback' => [$this, 'getEmailThemePreviewCss'],
'update_callback' => null,
'schema' => Builder::string()->toArray(),
]
);
}
public function initialize(): void {
register_rest_field(
'wp_template',
'email_theme_css',
array(
'get_callback' => array( $this, 'getEmailThemePreviewCss' ),
'update_callback' => null,
'schema' => Builder::string()->toArray(),
)
);
}
/**
* Generates CSS for preview of email theme
* They are applied in the preview BLockPreview in template selection
*/
public function getEmailThemePreviewCss($template): string {
$editorTheme = clone $this->themeController->getTheme();
$templateTheme = $this->templates->getBlockTemplateTheme($template['id'], $template['wp_id']);
if (is_array($templateTheme)) {
$editorTheme->merge(new WP_Theme_JSON($templateTheme, 'custom'));
}
$editorSettings = $this->settingsController->getSettings();
$additionalCSS = '';
foreach ($editorSettings['styles'] as $style) {
$additionalCSS .= $style['css'];
}
// Set proper content width for previews
$layoutSettings = $this->themeController->getLayoutSettings();
$additionalCSS .= ".is-root-container { width: {$layoutSettings['contentSize']}; margin: 0 auto; }";
return $editorTheme->get_stylesheet() . $additionalCSS;
}
/**
* Generates CSS for preview of email theme
* They are applied in the preview BLockPreview in template selection
*/
public function getEmailThemePreviewCss( $template ): string {
$editorTheme = clone $this->themeController->getTheme();
$templateTheme = $this->templates->getBlockTemplateTheme( $template['id'], $template['wp_id'] );
if ( is_array( $templateTheme ) ) {
$editorTheme->merge( new WP_Theme_JSON( $templateTheme, 'custom' ) );
}
$editorSettings = $this->settingsController->getSettings();
$additionalCSS = '';
foreach ( $editorSettings['styles'] as $style ) {
$additionalCSS .= $style['css'];
}
// Set proper content width for previews
$layoutSettings = $this->themeController->getLayoutSettings();
$additionalCSS .= ".is-root-container { width: {$layoutSettings['contentSize']}; margin: 0 auto; }";
return $editorTheme->get_stylesheet() . $additionalCSS;
}
}

View File

@ -7,289 +7,292 @@ use WP_Block_Template;
// phpcs:disable Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
class Templates {
const MAILPOET_EMAIL_META_THEME_TYPE = 'mailpoet_email_theme';
const MAILPOET_TEMPLATE_EMPTY_THEME = ['version' => 2]; // The version 2 is important to merge themes correctly
const MAILPOET_EMAIL_META_THEME_TYPE = 'mailpoet_email_theme';
const MAILPOET_TEMPLATE_EMPTY_THEME = array( 'version' => 2 ); // The version 2 is important to merge themes correctly
private Utils $utils;
private string $pluginSlug = 'mailpoet/mailpoet';
private string $postType = 'mailpoet_email';
private string $templateDirectory;
private array $templates = [];
private array $themeJson = [];
private Utils $utils;
private string $pluginSlug = 'mailpoet/mailpoet';
private string $postType = 'mailpoet_email';
private string $templateDirectory;
private array $templates = array();
private array $themeJson = array();
public function __construct(
Utils $utils
) {
$this->utils = $utils;
$this->templateDirectory = dirname(__FILE__) . DIRECTORY_SEPARATOR;
}
public function __construct(
Utils $utils
) {
$this->utils = $utils;
$this->templateDirectory = __DIR__ . DIRECTORY_SEPARATOR;
}
public function initialize(): void {
add_filter('pre_get_block_file_template', [$this, 'getBlockFileTemplate'], 10, 3);
add_filter('get_block_templates', [$this, 'addBlockTemplates'], 10, 3);
add_filter('theme_templates', [$this, 'addThemeTemplates'], 10, 4); // Needed when saving post template association
add_filter('get_block_template', [$this, 'addBlockTemplateDetails'], 10, 1);
add_filter('rest_pre_insert_wp_template', [$this, 'forcePostContent'], 9, 1);
$this->initializeTemplates();
$this->initializeApi();
}
public function initialize(): void {
add_filter( 'pre_get_block_file_template', array( $this, 'getBlockFileTemplate' ), 10, 3 );
add_filter( 'get_block_templates', array( $this, 'addBlockTemplates' ), 10, 3 );
add_filter( 'theme_templates', array( $this, 'addThemeTemplates' ), 10, 4 ); // Needed when saving post template association
add_filter( 'get_block_template', array( $this, 'addBlockTemplateDetails' ), 10, 1 );
add_filter( 'rest_pre_insert_wp_template', array( $this, 'forcePostContent' ), 9, 1 );
$this->initializeTemplates();
$this->initializeApi();
}
/**
* Get a block template by ID.
*/
public function getBlockTemplate($templateId) {
$templates = $this->getBlockTemplates();
return $templates[$templateId] ?? null;
}
/**
* Get a block template by ID.
*/
public function getBlockTemplate( $templateId ) {
$templates = $this->getBlockTemplates();
return $templates[ $templateId ] ?? null;
}
/**
* Get a predefined or user defined theme for a block template.
*
* @param string $templateId
* @param int|null $templateWpId
* @return array
*/
public function getBlockTemplateTheme($templateId, $templateWpId = null) {
// First check if there is a user updated theme saved
$theme = $this->getCustomTemplateTheme($templateWpId);
/**
* Get a predefined or user defined theme for a block template.
*
* @param string $templateId
* @param int|null $templateWpId
* @return array
*/
public function getBlockTemplateTheme( $templateId, $templateWpId = null ) {
// First check if there is a user updated theme saved
$theme = $this->getCustomTemplateTheme( $templateWpId );
if ($theme) {
return $theme;
}
if ( $theme ) {
return $theme;
}
// If there is no user edited theme, look for default template themes in files.
['prefix' => $templatePrefix, 'slug' => $templateSlug] = $this->utils->getTemplateIdParts($templateId);
// If there is no user edited theme, look for default template themes in files.
['prefix' => $templatePrefix, 'slug' => $templateSlug] = $this->utils->getTemplateIdParts( $templateId );
if ($this->pluginSlug !== $templatePrefix) {
return self::MAILPOET_TEMPLATE_EMPTY_THEME;
}
if ( $this->pluginSlug !== $templatePrefix ) {
return self::MAILPOET_TEMPLATE_EMPTY_THEME;
}
if (!isset($this->themeJson[$templateSlug])) {
$jsonFile = $this->templateDirectory . $templateSlug . '.json';
if ( ! isset( $this->themeJson[ $templateSlug ] ) ) {
$jsonFile = $this->templateDirectory . $templateSlug . '.json';
if (file_exists($jsonFile)) {
$this->themeJson[$templateSlug] = json_decode((string)file_get_contents($jsonFile), true);
}
}
if ( file_exists( $jsonFile ) ) {
$this->themeJson[ $templateSlug ] = json_decode( (string) file_get_contents( $jsonFile ), true );
}
}
return $this->themeJson[$templateSlug] ?? self::MAILPOET_TEMPLATE_EMPTY_THEME;
}
return $this->themeJson[ $templateSlug ] ?? self::MAILPOET_TEMPLATE_EMPTY_THEME;
}
public function getBlockFileTemplate($return, $templateId, $template_type) {
['prefix' => $templatePrefix, 'slug' => $templateSlug] = $this->utils->getTemplateIdParts($templateId);
public function getBlockFileTemplate( $return, $templateId, $template_type ) {
['prefix' => $templatePrefix, 'slug' => $templateSlug] = $this->utils->getTemplateIdParts( $templateId );
if ($this->pluginSlug !== $templatePrefix) {
return $return;
}
if ( $this->pluginSlug !== $templatePrefix ) {
return $return;
}
$templatePath = $templateSlug . '.html';
$templatePath = $templateSlug . '.html';
if (!is_readable($this->templateDirectory . $templatePath)) {
return $return;
}
if ( ! is_readable( $this->templateDirectory . $templatePath ) ) {
return $return;
}
return $this->getBlockTemplateFromFile($templatePath);
}
return $this->getBlockTemplateFromFile( $templatePath );
}
public function addBlockTemplates($query_result, $query, $template_type) {
if ('wp_template' !== $template_type) {
return $query_result;
}
public function addBlockTemplates( $query_result, $query, $template_type ) {
if ( 'wp_template' !== $template_type ) {
return $query_result;
}
$post_type = isset($query['post_type']) ? $query['post_type'] : '';
$post_type = isset( $query['post_type'] ) ? $query['post_type'] : '';
if ($post_type && $post_type !== $this->postType) {
return $query_result;
}
if ( $post_type && $post_type !== $this->postType ) {
return $query_result;
}
foreach ($this->getBlockTemplates() as $blockTemplate) {
$fits_slug_query = !isset($query['slug__in']) || in_array($blockTemplate->slug, $query['slug__in'], true);
$fits_area_query = !isset($query['area']) || ( property_exists($blockTemplate, 'area') && $blockTemplate->area === $query['area'] );
$should_include = $fits_slug_query && $fits_area_query;
foreach ( $this->getBlockTemplates() as $blockTemplate ) {
$fits_slug_query = ! isset( $query['slug__in'] ) || in_array( $blockTemplate->slug, $query['slug__in'], true );
$fits_area_query = ! isset( $query['area'] ) || ( property_exists( $blockTemplate, 'area' ) && $blockTemplate->area === $query['area'] );
$should_include = $fits_slug_query && $fits_area_query;
if ($should_include) {
$query_result[] = $blockTemplate;
}
}
if ( $should_include ) {
$query_result[] = $blockTemplate;
}
}
return $query_result;
}
return $query_result;
}
public function addThemeTemplates($templates, $theme, $post, $post_type) {
if ($post_type && $post_type !== $this->postType) {
return $templates;
}
foreach ($this->getBlockTemplates() as $blockTemplate) {
$templates[$blockTemplate->slug] = $blockTemplate;
}
return $templates;
}
public function addThemeTemplates( $templates, $theme, $post, $post_type ) {
if ( $post_type && $post_type !== $this->postType ) {
return $templates;
}
foreach ( $this->getBlockTemplates() as $blockTemplate ) {
$templates[ $blockTemplate->slug ] = $blockTemplate;
}
return $templates;
}
/**
* This is a workaround to ensure the post object passed to `inject_ignored_hooked_blocks_metadata_attributes` contains
* content to prevent the template being empty when saved. The issue currently occurs when WooCommerce enables block hooks,
* and when older versions of `inject_ignored_hooked_blocks_metadata_attributes` are
* used (before https://github.com/WordPress/WordPress/commit/725f302121c84c648c38789b2e88dbd1eb41fa48).
* This can be removed in the future.
*
* To test the issue create a new email, revert template changes, save a color change, then save a color change again.
* When you refresh if the post is blank, the issue is present.
*
* @param \stdClass $changes
*/
public function forcePostContent($changes) {
if (empty($changes->post_content) && !empty($changes->ID)) {
// Find the existing post object.
$post = get_post($changes->ID);
if ($post && !empty($post->post_content)) {
$changes->post_content = $post->post_content;
}
}
return $changes;
}
/**
* This is a workaround to ensure the post object passed to `inject_ignored_hooked_blocks_metadata_attributes` contains
* content to prevent the template being empty when saved. The issue currently occurs when WooCommerce enables block hooks,
* and when older versions of `inject_ignored_hooked_blocks_metadata_attributes` are
* used (before https://github.com/WordPress/WordPress/commit/725f302121c84c648c38789b2e88dbd1eb41fa48).
* This can be removed in the future.
*
* To test the issue create a new email, revert template changes, save a color change, then save a color change again.
* When you refresh if the post is blank, the issue is present.
*
* @param \stdClass $changes
*/
public function forcePostContent( $changes ) {
if ( empty( $changes->post_content ) && ! empty( $changes->ID ) ) {
// Find the existing post object.
$post = get_post( $changes->ID );
if ( $post && ! empty( $post->post_content ) ) {
$changes->post_content = $post->post_content;
}
}
return $changes;
}
/**
* Add details to templates in editor.
*
* @param WP_Block_Template $block_template Block template object.
* @return WP_Block_Template
*/
public function addBlockTemplateDetails($block_template) {
if (!$block_template || !isset($this->templates[$block_template->slug])) {
return $block_template;
}
if (empty($block_template->title)) {
$block_template->title = $this->templates[$block_template->slug]['title'];
}
if (empty($block_template->description)) {
$block_template->description = $this->templates[$block_template->slug]['description'];
}
return $block_template;
}
/**
* Add details to templates in editor.
*
* @param WP_Block_Template $block_template Block template object.
* @return WP_Block_Template
*/
public function addBlockTemplateDetails( $block_template ) {
if ( ! $block_template || ! isset( $this->templates[ $block_template->slug ] ) ) {
return $block_template;
}
if ( empty( $block_template->title ) ) {
$block_template->title = $this->templates[ $block_template->slug ]['title'];
}
if ( empty( $block_template->description ) ) {
$block_template->description = $this->templates[ $block_template->slug ]['description'];
}
return $block_template;
}
/**
* Initialize template details. This is done at runtime because of localisation.
*/
private function initializeTemplates(): void {
$this->templates['email-general'] = [
'title' => __('General Email', 'mailpoet'),
'description' => __('A general template for emails.', 'mailpoet'),
];
$this->templates['simple-light'] = [
'title' => __('Simple Light', 'mailpoet'),
'description' => __('A basic template with header and footer.', 'mailpoet'),
];
}
/**
* Initialize template details. This is done at runtime because of localisation.
*/
private function initializeTemplates(): void {
$this->templates['email-general'] = array(
'title' => __( 'General Email', 'mailpoet' ),
'description' => __( 'A general template for emails.', 'mailpoet' ),
);
$this->templates['simple-light'] = array(
'title' => __( 'Simple Light', 'mailpoet' ),
'description' => __( 'A basic template with header and footer.', 'mailpoet' ),
);
}
private function initializeApi(): void {
register_post_meta(
'wp_template',
self::MAILPOET_EMAIL_META_THEME_TYPE,
[
'show_in_rest' => [
'schema' => (new Email_Styles_Schema())->getSchema(),
],
'single' => true,
'type' => 'object',
'default' => self::MAILPOET_TEMPLATE_EMPTY_THEME,
]
);
register_rest_field(
'wp_template',
self::MAILPOET_EMAIL_META_THEME_TYPE,
[
'get_callback' => function($object) {
return $this->getBlockTemplateTheme($object['id'], $object['wp_id']);
},
'update_callback' => function($value, $template) {
return update_post_meta($template->wp_id, self::MAILPOET_EMAIL_META_THEME_TYPE, $value);
},
'schema' => (new Email_Styles_Schema())->getSchema(),
]
);
}
private function initializeApi(): void {
register_post_meta(
'wp_template',
self::MAILPOET_EMAIL_META_THEME_TYPE,
array(
'show_in_rest' => array(
'schema' => ( new Email_Styles_Schema() )->getSchema(),
),
'single' => true,
'type' => 'object',
'default' => self::MAILPOET_TEMPLATE_EMPTY_THEME,
)
);
register_rest_field(
'wp_template',
self::MAILPOET_EMAIL_META_THEME_TYPE,
array(
'get_callback' => function ( $object ) {
return $this->getBlockTemplateTheme( $object['id'], $object['wp_id'] );
},
'update_callback' => function ( $value, $template ) {
return update_post_meta( $template->wp_id, self::MAILPOET_EMAIL_META_THEME_TYPE, $value );
},
'schema' => ( new Email_Styles_Schema() )->getSchema(),
)
);
}
/**
* Gets block templates indexed by ID.
*/
private function getBlockTemplates() {
$blockTemplates = array_map(function($templateSlug) {
return $this->getBlockTemplateFromFile($templateSlug . '.html');
}, array_keys($this->templates));
$customTemplates = $this->getCustomTemplates(); // From the DB.
$customTemplateIds = wp_list_pluck($customTemplates, 'id');
/**
* Gets block templates indexed by ID.
*/
private function getBlockTemplates() {
$blockTemplates = array_map(
function ( $templateSlug ) {
return $this->getBlockTemplateFromFile( $templateSlug . '.html' );
},
array_keys( $this->templates )
);
$customTemplates = $this->getCustomTemplates(); // From the DB.
$customTemplateIds = wp_list_pluck( $customTemplates, 'id' );
// Combine to remove duplicates if a custom template has the same ID as a file template.
return array_column(
array_merge(
$customTemplates,
array_filter(
$blockTemplates,
function($blockTemplate) use ($customTemplateIds) {
return !in_array($blockTemplate->id, $customTemplateIds, true);
}
),
),
null,
'id'
);
}
// Combine to remove duplicates if a custom template has the same ID as a file template.
return array_column(
array_merge(
$customTemplates,
array_filter(
$blockTemplates,
function ( $blockTemplate ) use ( $customTemplateIds ) {
return ! in_array( $blockTemplate->id, $customTemplateIds, true );
}
),
),
null,
'id'
);
}
private function getBlockTemplateFromFile(string $template) {
$template_slug = $this->utils->getBlockTemplateSlugFromPath($template);
$templateObject = (object)[
'slug' => $template_slug,
'id' => $this->pluginSlug . '//' . $template_slug,
'title' => $this->templates[$template_slug]['title'] ?? '',
'description' => $this->templates[$template_slug]['description'] ?? '',
'path' => $this->templateDirectory . $template,
'type' => 'wp_template',
'theme' => $this->pluginSlug,
'source' => 'plugin',
'post_types' => [
$this->postType,
],
];
return $this->utils->buildBlockTemplateFromFile($templateObject);
}
private function getBlockTemplateFromFile( string $template ) {
$template_slug = $this->utils->getBlockTemplateSlugFromPath( $template );
$templateObject = (object) array(
'slug' => $template_slug,
'id' => $this->pluginSlug . '//' . $template_slug,
'title' => $this->templates[ $template_slug ]['title'] ?? '',
'description' => $this->templates[ $template_slug ]['description'] ?? '',
'path' => $this->templateDirectory . $template,
'type' => 'wp_template',
'theme' => $this->pluginSlug,
'source' => 'plugin',
'post_types' => array(
$this->postType,
),
);
return $this->utils->buildBlockTemplateFromFile( $templateObject );
}
private function getCustomTemplates($slugs = [], $template_type = 'wp_template') {
$check_query_args = [
'post_type' => $template_type,
'posts_per_page' => -1,
'no_found_rows' => true,
'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
[
'taxonomy' => 'wp_theme',
'field' => 'name',
'terms' => [ $this->pluginSlug, get_stylesheet() ],
],
],
];
private function getCustomTemplates( $slugs = array(), $template_type = 'wp_template' ) {
$check_query_args = array(
'post_type' => $template_type,
'posts_per_page' => -1,
'no_found_rows' => true,
'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
array(
'taxonomy' => 'wp_theme',
'field' => 'name',
'terms' => array( $this->pluginSlug, get_stylesheet() ),
),
),
);
if (is_array($slugs) && count($slugs) > 0) {
$check_query_args['post_name__in'] = $slugs;
}
if ( is_array( $slugs ) && count( $slugs ) > 0 ) {
$check_query_args['post_name__in'] = $slugs;
}
$check_query = new \WP_Query($check_query_args);
$custom_templates = $check_query->posts;
$check_query = new \WP_Query( $check_query_args );
$custom_templates = $check_query->posts;
return array_map(
function($custom_template) {
return $this->utils->buildBlockTemplateFromPost($custom_template);
},
$custom_templates
);
}
return array_map(
function ( $custom_template ) {
return $this->utils->buildBlockTemplateFromPost( $custom_template );
},
$custom_templates
);
}
private function getCustomTemplateTheme($templateWpId) {
if (!$templateWpId) {
return null;
}
$theme = get_post_meta($templateWpId, self::MAILPOET_EMAIL_META_THEME_TYPE, true);
if (is_array($theme) && isset($theme['styles'])) {
return $theme;
}
return null;
}
private function getCustomTemplateTheme( $templateWpId ) {
if ( ! $templateWpId ) {
return null;
}
$theme = get_post_meta( $templateWpId, self::MAILPOET_EMAIL_META_THEME_TYPE, true );
if ( is_array( $theme ) && isset( $theme['styles'] ) ) {
return $theme;
}
return null;
}
}

View File

@ -7,88 +7,88 @@ use WP_Error;
// phpcs:disable Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
class Utils {
/**
* Gets the prefix and slug from the template ID.
*
* @param string $templateId Id of the template in prefix//slug format.
* @return array Associative array with keys 'prefix' and 'slug'.
*/
public function getTemplateIdParts($templateId) {
$template_name_parts = explode('//', $templateId);
/**
* Gets the prefix and slug from the template ID.
*
* @param string $templateId Id of the template in prefix//slug format.
* @return array Associative array with keys 'prefix' and 'slug'.
*/
public function getTemplateIdParts( $templateId ) {
$template_name_parts = explode( '//', $templateId );
if (count($template_name_parts) < 2) {
return [
'prefix' => '',
'slug' => '',
];
}
if ( count( $template_name_parts ) < 2 ) {
return array(
'prefix' => '',
'slug' => '',
);
}
return [
'prefix' => $template_name_parts[0],
'slug' => $template_name_parts[1],
];
}
return array(
'prefix' => $template_name_parts[0],
'slug' => $template_name_parts[1],
);
}
public static function getBlockTemplateSlugFromPath($path) {
return basename($path, '.html');
}
public static function getBlockTemplateSlugFromPath( $path ) {
return basename( $path, '.html' );
}
public function buildBlockTemplateFromPost($post) {
$terms = get_the_terms($post, 'wp_theme');
public function buildBlockTemplateFromPost( $post ) {
$terms = get_the_terms( $post, 'wp_theme' );
if (is_wp_error($terms)) {
return $terms;
}
if ( is_wp_error( $terms ) ) {
return $terms;
}
if (!$terms) {
return new WP_Error('template_missing_theme', 'No theme is defined for this template.');
}
if ( ! $terms ) {
return new WP_Error( 'template_missing_theme', 'No theme is defined for this template.' );
}
$templatePrefix = $terms[0]->name;
$templateSlug = $post->post_name;
$templateId = $templatePrefix . '//' . $templateSlug;
$templatePrefix = $terms[0]->name;
$templateSlug = $post->post_name;
$templateId = $templatePrefix . '//' . $templateSlug;
$template = new WP_Block_Template();
$template->wp_id = $post->ID;
$template->id = $templateId;
$template->theme = $templatePrefix;
$template->content = $post->post_content ? $post->post_content : '<p>empty</p>';
$template->slug = $templateSlug;
$template->source = 'custom';
$template->type = $post->post_type;
$template->description = $post->post_excerpt;
$template->title = $post->post_title;
$template->status = $post->post_status;
$template->has_theme_file = false;
$template->is_custom = true;
$template->post_types = [];
$template = new WP_Block_Template();
$template->wp_id = $post->ID;
$template->id = $templateId;
$template->theme = $templatePrefix;
$template->content = $post->post_content ? $post->post_content : '<p>empty</p>';
$template->slug = $templateSlug;
$template->source = 'custom';
$template->type = $post->post_type;
$template->description = $post->post_excerpt;
$template->title = $post->post_title;
$template->status = $post->post_status;
$template->has_theme_file = false;
$template->is_custom = true;
$template->post_types = array();
if ('wp_template_part' === $post->post_type) {
$type_terms = get_the_terms($post, 'wp_template_part_area');
if ( 'wp_template_part' === $post->post_type ) {
$type_terms = get_the_terms( $post, 'wp_template_part_area' );
if (!is_wp_error($type_terms) && false !== $type_terms) {
$template->area = $type_terms[0]->name;
}
}
if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
$template->area = $type_terms[0]->name;
}
}
return $template;
}
return $template;
}
public function buildBlockTemplateFromFile($templateObject): WP_Block_Template {
$template = new WP_Block_Template();
$template->id = $templateObject->id;
$template->theme = $templateObject->theme;
$template->content = (string)file_get_contents($templateObject->path);
$template->source = $templateObject->source;
$template->slug = $templateObject->slug;
$template->type = $templateObject->type;
$template->title = $templateObject->title;
$template->description = $templateObject->description;
$template->status = 'publish';
$template->has_theme_file = false;
$template->post_types = $templateObject->post_types;
$template->is_custom = false; // Templates are only custom if they are loaded from the DB.
$template->area = 'uncategorized';
return $template;
}
public function buildBlockTemplateFromFile( $templateObject ): WP_Block_Template {
$template = new WP_Block_Template();
$template->id = $templateObject->id;
$template->theme = $templateObject->theme;
$template->content = (string) file_get_contents( $templateObject->path );
$template->source = $templateObject->source;
$template->slug = $templateObject->slug;
$template->type = $templateObject->type;
$template->title = $templateObject->title;
$template->description = $templateObject->description;
$template->status = 'publish';
$template->has_theme_file = false;
$template->post_types = $templateObject->post_types;
$template->is_custom = false; // Templates are only custom if they are loaded from the DB.
$template->area = 'uncategorized';
return $template;
}
}

View File

@ -5,22 +5,22 @@ namespace MailPoet\EmailEditor\Engine;
use MailPoet\EmailEditor\Validator\Builder;
class Email_Api_Controller {
/**
* @return array - Email specific data such styles.
*/
public function getEmailData(): array {
// Here comes code getting Email specific data that will be passed on 'email_data' attribute
return [];
}
/**
* @return array - Email specific data such styles.
*/
public function getEmailData(): array {
// Here comes code getting Email specific data that will be passed on 'email_data' attribute
return array();
}
/**
* Update Email specific data we store.
*/
public function saveEmailData(array $data, \WP_Post $emailPost): void {
// Here comes code saving of Email specific data that will be passed on 'email_data' attribute
}
/**
* Update Email specific data we store.
*/
public function saveEmailData( array $data, \WP_Post $emailPost ): void {
// Here comes code saving of Email specific data that will be passed on 'email_data' attribute
}
public function getEmailDataSchema(): array {
return Builder::object()->toArray();
}
public function getEmailDataSchema(): array {
return Builder::object()->toArray();
}
}

View File

@ -13,116 +13,123 @@ use WP_Theme_JSON;
* See register_post_type for details about EmailPostType args.
*/
class Email_Editor {
public const MAILPOET_EMAIL_META_THEME_TYPE = 'mailpoet_email_theme';
public const MAILPOET_EMAIL_META_THEME_TYPE = 'mailpoet_email_theme';
private Email_Api_Controller $emailApiController;
private Templates $templates;
private Template_Preview $templatePreview;
private Patterns $patterns;
private Settings_Controller $settingsController;
private Email_Api_Controller $emailApiController;
private Templates $templates;
private Template_Preview $templatePreview;
private Patterns $patterns;
private Settings_Controller $settingsController;
public function __construct(
Email_Api_Controller $emailApiController,
Templates $templates,
Template_Preview $templatePreview,
Patterns $patterns,
Settings_Controller $settingsController
) {
$this->emailApiController = $emailApiController;
$this->templates = $templates;
$this->templatePreview = $templatePreview;
$this->patterns = $patterns;
$this->settingsController = $settingsController;
}
public function __construct(
Email_Api_Controller $emailApiController,
Templates $templates,
Template_Preview $templatePreview,
Patterns $patterns,
Settings_Controller $settingsController
) {
$this->emailApiController = $emailApiController;
$this->templates = $templates;
$this->templatePreview = $templatePreview;
$this->patterns = $patterns;
$this->settingsController = $settingsController;
}
public function initialize(): void {
do_action('mailpoet_email_editor_initialized');
add_filter('mailpoet_email_editor_rendering_theme_styles', [$this, 'extendEmailThemeStyles'], 10, 2);
$this->registerBlockTemplates();
$this->registerBlockPatterns();
$this->registerEmailPostTypes();
$this->registerEmailPostSendStatus();
$isEditorPage = apply_filters('mailpoet_is_email_editor_page', false);
if ($isEditorPage) {
$this->extendEmailPostApi();
$this->settingsController->init();
}
}
public function initialize(): void {
do_action( 'mailpoet_email_editor_initialized' );
add_filter( 'mailpoet_email_editor_rendering_theme_styles', array( $this, 'extendEmailThemeStyles' ), 10, 2 );
$this->registerBlockTemplates();
$this->registerBlockPatterns();
$this->registerEmailPostTypes();
$this->registerEmailPostSendStatus();
$isEditorPage = apply_filters( 'mailpoet_is_email_editor_page', false );
if ( $isEditorPage ) {
$this->extendEmailPostApi();
$this->settingsController->init();
}
}
private function registerBlockTemplates(): void {
// Since we cannot currently disable blocks in the editor for specific templates, disable templates when viewing site editor. @see https://github.com/WordPress/gutenberg/issues/41062
if (strstr(wp_unslash($_SERVER['REQUEST_URI'] ?? ''), 'site-editor.php') === false) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$this->templates->initialize();
$this->templatePreview->initialize();
}
}
private function registerBlockTemplates(): void {
// Since we cannot currently disable blocks in the editor for specific templates, disable templates when viewing site editor. @see https://github.com/WordPress/gutenberg/issues/41062
if ( strstr( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ), 'site-editor.php' ) === false ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$this->templates->initialize();
$this->templatePreview->initialize();
}
}
private function registerBlockPatterns(): void {
$this->patterns->initialize();
}
private function registerBlockPatterns(): void {
$this->patterns->initialize();
}
/**
* Register all custom post types that should be edited via the email editor
* The post types are added via mailpoet_email_editor_post_types filter.
*/
private function registerEmailPostTypes(): void {
foreach ($this->getPostTypes() as $postType) {
register_post_type(
$postType['name'],
array_merge($this->getDefaultEmailPostArgs(), $postType['args'])
);
}
}
/**
* Register all custom post types that should be edited via the email editor
* The post types are added via mailpoet_email_editor_post_types filter.
*/
private function registerEmailPostTypes(): void {
foreach ( $this->getPostTypes() as $postType ) {
register_post_type(
$postType['name'],
array_merge( $this->getDefaultEmailPostArgs(), $postType['args'] )
);
}
}
/**
* @phpstan-return EmailPostType[]
*/
private function getPostTypes(): array {
$postTypes = [];
return apply_filters('mailpoet_email_editor_post_types', $postTypes);
}
/**
* @phpstan-return EmailPostType[]
*/
private function getPostTypes(): array {
$postTypes = array();
return apply_filters( 'mailpoet_email_editor_post_types', $postTypes );
}
private function getDefaultEmailPostArgs(): array {
return [
'public' => false,
'hierarchical' => false,
'show_ui' => true,
'show_in_menu' => false,
'show_in_nav_menus' => false,
'supports' => ['editor', 'title', 'custom-fields'], // 'custom-fields' is required for loading meta fields via API
'has_archive' => true,
'show_in_rest' => true, // Important to enable Gutenberg editor
];
}
private function getDefaultEmailPostArgs(): array {
return array(
'public' => false,
'hierarchical' => false,
'show_ui' => true,
'show_in_menu' => false,
'show_in_nav_menus' => false,
'supports' => array( 'editor', 'title', 'custom-fields' ), // 'custom-fields' is required for loading meta fields via API
'has_archive' => true,
'show_in_rest' => true, // Important to enable Gutenberg editor
);
}
private function registerEmailPostSendStatus(): void {
register_post_status('sent', [
'public' => false,
'exclude_from_search' => true,
'internal' => true, // for now, we hide it, if we use the status in the listings we may flip this and following values
'show_in_admin_all_list' => false,
'show_in_admin_status_list' => false,
]);
}
private function registerEmailPostSendStatus(): void {
register_post_status(
'sent',
array(
'public' => false,
'exclude_from_search' => true,
'internal' => true, // for now, we hide it, if we use the status in the listings we may flip this and following values
'show_in_admin_all_list' => false,
'show_in_admin_status_list' => false,
)
);
}
public function extendEmailPostApi() {
$emailPostTypes = array_column($this->getPostTypes(), 'name');
register_rest_field($emailPostTypes, 'email_data', [
'get_callback' => [$this->emailApiController, 'getEmailData'],
'update_callback' => [$this->emailApiController, 'saveEmailData'],
'schema' => $this->emailApiController->getEmailDataSchema(),
]);
}
public function extendEmailPostApi() {
$emailPostTypes = array_column( $this->getPostTypes(), 'name' );
register_rest_field(
$emailPostTypes,
'email_data',
array(
'get_callback' => array( $this->emailApiController, 'getEmailData' ),
'update_callback' => array( $this->emailApiController, 'saveEmailData' ),
'schema' => $this->emailApiController->getEmailDataSchema(),
)
);
}
public function getEmailThemeDataSchema(): array {
return (new Email_Styles_Schema())->getSchema();
}
public function getEmailThemeDataSchema(): array {
return ( new Email_Styles_Schema() )->getSchema();
}
public function extendEmailThemeStyles(WP_Theme_JSON $theme, WP_Post $post): WP_Theme_JSON {
$emailTheme = get_post_meta($post->ID, Email_Editor::MAILPOET_EMAIL_META_THEME_TYPE, true);
if ($emailTheme && is_array($emailTheme)) {
$theme->merge(new WP_Theme_JSON($emailTheme));
}
return $theme;
}
public function extendEmailThemeStyles( WP_Theme_JSON $theme, WP_Post $post ): WP_Theme_JSON {
$emailTheme = get_post_meta( $post->ID, self::MAILPOET_EMAIL_META_THEME_TYPE, true );
if ( $emailTheme && is_array( $emailTheme ) ) {
$theme->merge( new WP_Theme_JSON( $emailTheme ) );
}
return $theme;
}
}

View File

@ -5,64 +5,96 @@ namespace MailPoet\EmailEditor\Engine;
use MailPoet\EmailEditor\Validator\Builder;
class Email_Styles_Schema {
public function getSchema(): array {
$typographyProps = Builder::object([
'fontFamily' => Builder::string()->nullable(),
'fontSize' => Builder::string()->nullable(),
'fontStyle' => Builder::string()->nullable(),
'fontWeight' => Builder::string()->nullable(),
'letterSpacing' => Builder::string()->nullable(),
'lineHeight' => Builder::string()->nullable(),
'textTransform' => Builder::string()->nullable(),
'textDecoration' => Builder::string()->nullable(),
])->nullable();
return Builder::object([
'version' => Builder::integer(),
'styles' => Builder::object([
'spacing' => Builder::object([
'padding' => Builder::object([
'top' => Builder::string(),
'right' => Builder::string(),
'bottom' => Builder::string(),
'left' => Builder::string(),
])->nullable(),
'blockGap' => Builder::string()->nullable(),
])->nullable(),
'color' => Builder::object([
'background' => Builder::string()->nullable(),
'text' => Builder::string()->nullable(),
])->nullable(),
'typography' => $typographyProps,
'elements' => Builder::object([
'heading' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
'button' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
'link' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
'h1' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
'h2' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
'h3' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
'h4' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
'h5' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
'h6' => Builder::object([
'typography' => $typographyProps,
])->nullable(),
])->nullable(),
])->nullable(),
])->toArray();
}
public function getSchema(): array {
$typographyProps = Builder::object(
array(
'fontFamily' => Builder::string()->nullable(),
'fontSize' => Builder::string()->nullable(),
'fontStyle' => Builder::string()->nullable(),
'fontWeight' => Builder::string()->nullable(),
'letterSpacing' => Builder::string()->nullable(),
'lineHeight' => Builder::string()->nullable(),
'textTransform' => Builder::string()->nullable(),
'textDecoration' => Builder::string()->nullable(),
)
)->nullable();
return Builder::object(
array(
'version' => Builder::integer(),
'styles' => Builder::object(
array(
'spacing' => Builder::object(
array(
'padding' => Builder::object(
array(
'top' => Builder::string(),
'right' => Builder::string(),
'bottom' => Builder::string(),
'left' => Builder::string(),
)
)->nullable(),
'blockGap' => Builder::string()->nullable(),
)
)->nullable(),
'color' => Builder::object(
array(
'background' => Builder::string()->nullable(),
'text' => Builder::string()->nullable(),
)
)->nullable(),
'typography' => $typographyProps,
'elements' => Builder::object(
array(
'heading' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
'button' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
'link' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
'h1' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
'h2' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
'h3' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
'h4' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
'h5' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
'h6' => Builder::object(
array(
'typography' => $typographyProps,
)
)->nullable(),
)
)->nullable(),
)
)->nullable(),
)
)->toArray();
}
}

View File

@ -4,130 +4,130 @@ namespace MailPoet\EmailEditor\Engine;
class Settings_Controller {
const ALLOWED_BLOCK_TYPES = [
'core/button',
'core/buttons',
'core/paragraph',
'core/heading',
'core/column',
'core/columns',
'core/image',
'core/list',
'core/list-item',
'core/group',
'core/spacer'
];
const ALLOWED_BLOCK_TYPES = array(
'core/button',
'core/buttons',
'core/paragraph',
'core/heading',
'core/column',
'core/columns',
'core/image',
'core/list',
'core/list-item',
'core/group',
'core/spacer',
);
const DEFAULT_SETTINGS = [
'enableCustomUnits' => ['px', '%'],
];
const DEFAULT_SETTINGS = array(
'enableCustomUnits' => array( 'px', '%' ),
);
private Theme_Controller $themeController;
private Theme_Controller $themeController;
private array $iframeAssets = [];
private array $iframeAssets = array();
/**
* @param Theme_Controller $themeController
*/
public function __construct(
Theme_Controller $themeController
) {
$this->themeController = $themeController;
}
/**
* @param Theme_Controller $themeController
*/
public function __construct(
Theme_Controller $themeController
) {
$this->themeController = $themeController;
}
public function init() {
// We need to initialize these assets early because they are read from global variables $wp_styles and $wp_scripts
// and in later WordPress page load pages they contain stuff we don't want (e.g. html for admin login popup)
// in the post editor this is called directly in post.php
$this->iframeAssets = _wp_get_iframed_editor_assets();
}
public function init() {
// We need to initialize these assets early because they are read from global variables $wp_styles and $wp_scripts
// and in later WordPress page load pages they contain stuff we don't want (e.g. html for admin login popup)
// in the post editor this is called directly in post.php
$this->iframeAssets = _wp_get_iframed_editor_assets();
}
public function getSettings(): array {
$coreDefaultSettings = \get_default_block_editor_settings();
$themeSettings = $this->themeController->getSettings();
public function getSettings(): array {
$coreDefaultSettings = \get_default_block_editor_settings();
$themeSettings = $this->themeController->getSettings();
$settings = array_merge($coreDefaultSettings, self::DEFAULT_SETTINGS);
$settings['allowedBlockTypes'] = self::ALLOWED_BLOCK_TYPES;
// Assets for iframe editor (component styles, scripts, etc.)
$settings['__unstableResolvedAssets'] = $this->iframeAssets;
$editorContentStyles = file_get_contents(__DIR__ . '/content-editor.css');
$sharesContentStyles = file_get_contents(__DIR__ . '/content-shared.css');
$settings['styles'] = [
['css' => $editorContentStyles],
['css' => $sharesContentStyles],
];
$settings = array_merge( $coreDefaultSettings, self::DEFAULT_SETTINGS );
$settings['allowedBlockTypes'] = self::ALLOWED_BLOCK_TYPES;
// Assets for iframe editor (component styles, scripts, etc.)
$settings['__unstableResolvedAssets'] = $this->iframeAssets;
$editorContentStyles = file_get_contents( __DIR__ . '/content-editor.css' );
$sharesContentStyles = file_get_contents( __DIR__ . '/content-shared.css' );
$settings['styles'] = array(
array( 'css' => $editorContentStyles ),
array( 'css' => $sharesContentStyles ),
);
$settings['__experimentalFeatures'] = $themeSettings;
// Controls which alignment options are available for blocks
$settings['supportsLayout'] = true; // Allow using default layouts
$settings['__unstableIsBlockBasedTheme'] = true; // For default setting this to true disables wide and full alignments
return $settings;
}
$settings['__experimentalFeatures'] = $themeSettings;
// Controls which alignment options are available for blocks
$settings['supportsLayout'] = true; // Allow using default layouts
$settings['__unstableIsBlockBasedTheme'] = true; // For default setting this to true disables wide and full alignments
return $settings;
}
/**
* @return array{contentSize: string, wideSize: string}
*/
public function getLayout(): array {
$layoutSettings = $this->themeController->getLayoutSettings();
return [
'contentSize' => $layoutSettings['contentSize'],
'wideSize' => $layoutSettings['wideSize'],
];
}
/**
* @return array{contentSize: string, wideSize: string}
*/
public function getLayout(): array {
$layoutSettings = $this->themeController->getLayoutSettings();
return array(
'contentSize' => $layoutSettings['contentSize'],
'wideSize' => $layoutSettings['wideSize'],
);
}
/**
* @return array{
* spacing: array{
* blockGap: string,
* padding: array{bottom: string, left: string, right: string, top: string}
* },
* color: array{
* background: string
* },
* typography: array{
* fontFamily: string
* }
* }
*/
public function getEmailStyles(): array {
$theme = $this->getTheme();
return $theme->get_data()['styles'];
}
/**
* @return array{
* spacing: array{
* blockGap: string,
* padding: array{bottom: string, left: string, right: string, top: string}
* },
* color: array{
* background: string
* },
* typography: array{
* fontFamily: string
* }
* }
*/
public function getEmailStyles(): array {
$theme = $this->getTheme();
return $theme->get_data()['styles'];
}
public function getLayoutWidthWithoutPadding(): string {
$styles = $this->getEmailStyles();
$layout = $this->getLayout();
$width = $this->parseNumberFromStringWithPixels($layout['contentSize']);
$width -= $this->parseNumberFromStringWithPixels($styles['spacing']['padding']['left']);
$width -= $this->parseNumberFromStringWithPixels($styles['spacing']['padding']['right']);
return "{$width}px";
}
public function getLayoutWidthWithoutPadding(): string {
$styles = $this->getEmailStyles();
$layout = $this->getLayout();
$width = $this->parseNumberFromStringWithPixels( $layout['contentSize'] );
$width -= $this->parseNumberFromStringWithPixels( $styles['spacing']['padding']['left'] );
$width -= $this->parseNumberFromStringWithPixels( $styles['spacing']['padding']['right'] );
return "{$width}px";
}
public function parseStylesToArray(string $styles): array {
$styles = explode(';', $styles);
$parsedStyles = [];
foreach ($styles as $style) {
$style = explode(':', $style);
if (count($style) === 2) {
$parsedStyles[trim($style[0])] = trim($style[1]);
}
}
return $parsedStyles;
}
public function parseStylesToArray( string $styles ): array {
$styles = explode( ';', $styles );
$parsedStyles = array();
foreach ( $styles as $style ) {
$style = explode( ':', $style );
if ( count( $style ) === 2 ) {
$parsedStyles[ trim( $style[0] ) ] = trim( $style[1] );
}
}
return $parsedStyles;
}
public function parseNumberFromStringWithPixels(string $string): float {
return (float)str_replace('px', '', $string);
}
public function parseNumberFromStringWithPixels( string $string ): float {
return (float) str_replace( 'px', '', $string );
}
public function getTheme(): \WP_Theme_JSON {
return $this->themeController->getTheme();
}
public function getTheme(): \WP_Theme_JSON {
return $this->themeController->getTheme();
}
public function translateSlugToFontSize(string $fontSize): string {
return $this->themeController->translateSlugToFontSize($fontSize);
}
public function translateSlugToFontSize( string $fontSize ): string {
return $this->themeController->translateSlugToFontSize( $fontSize );
}
public function translateSlugToColor(string $colorSlug): string {
return $this->themeController->translateSlugToColor($colorSlug);
}
public function translateSlugToColor( string $colorSlug ): string {
return $this->themeController->translateSlugToColor( $colorSlug );
}
}

View File

@ -11,233 +11,233 @@ use WP_Theme_JSON_Resolver;
* This class is responsible for accessing data defined by the theme.json.
*/
class Theme_Controller {
private WP_Theme_JSON $coreTheme;
private WP_Theme_JSON $baseTheme;
private WP_Theme_JSON $coreTheme;
private WP_Theme_JSON $baseTheme;
public function __construct() {
$this->coreTheme = WP_Theme_JSON_Resolver::get_core_data();
$this->baseTheme = new WP_Theme_JSON((array)json_decode((string)file_get_contents(dirname(__FILE__) . '/theme.json'), true), 'default');
}
public function __construct() {
$this->coreTheme = WP_Theme_JSON_Resolver::get_core_data();
$this->baseTheme = new WP_Theme_JSON( (array) json_decode( (string) file_get_contents( __DIR__ . '/theme.json' ), true ), 'default' );
}
/**
* Gets combined theme data from the core and base theme, merged with the currently rendered template.
*
* @return WP_Theme_JSON
*/
public function getTheme(): WP_Theme_JSON {
$theme = new WP_Theme_JSON();
$theme->merge($this->coreTheme);
$theme->merge($this->baseTheme);
/**
* Gets combined theme data from the core and base theme, merged with the currently rendered template.
*
* @return WP_Theme_JSON
*/
public function getTheme(): WP_Theme_JSON {
$theme = new WP_Theme_JSON();
$theme->merge( $this->coreTheme );
$theme->merge( $this->baseTheme );
if (Renderer::getTheme() !== null) {
$theme->merge(Renderer::getTheme());
}
if ( Renderer::getTheme() !== null ) {
$theme->merge( Renderer::getTheme() );
}
return apply_filters('mailpoet_email_editor_theme_json', $theme);
}
return apply_filters( 'mailpoet_email_editor_theme_json', $theme );
}
private function recursiveReplacePresets($values, $presets) {
foreach ($values as $key => $value) {
if (is_array($value)) {
$values[$key] = $this->recursiveReplacePresets($value, $presets);
} elseif (is_string($value)) {
$values[$key] = preg_replace(array_keys($presets), array_values($presets), $value);
} else {
$values[$key] = $value;
}
}
return $values;
}
private function recursiveReplacePresets( $values, $presets ) {
foreach ( $values as $key => $value ) {
if ( is_array( $value ) ) {
$values[ $key ] = $this->recursiveReplacePresets( $value, $presets );
} elseif ( is_string( $value ) ) {
$values[ $key ] = preg_replace( array_keys( $presets ), array_values( $presets ), $value );
} else {
$values[ $key ] = $value;
}
}
return $values;
}
private function recursiveExtractPresetVariables($styles) {
foreach ($styles as $key => $styleValue) {
if (is_array($styleValue)) {
$styles[$key] = $this->recursiveExtractPresetVariables($styleValue);
} elseif (strpos($styleValue, 'var:preset|') === 0) {
$styles[$key] = 'var(--wp--' . str_replace('|', '--', str_replace('var:', '', $styleValue)) . ')';
} else {
$styles[$key] = $styleValue;
}
}
return $styles;
}
private function recursiveExtractPresetVariables( $styles ) {
foreach ( $styles as $key => $styleValue ) {
if ( is_array( $styleValue ) ) {
$styles[ $key ] = $this->recursiveExtractPresetVariables( $styleValue );
} elseif ( strpos( $styleValue, 'var:preset|' ) === 0 ) {
$styles[ $key ] = 'var(--wp--' . str_replace( '|', '--', str_replace( 'var:', '', $styleValue ) ) . ')';
} else {
$styles[ $key ] = $styleValue;
}
}
return $styles;
}
/**
* Get styles for the e-mail.
*
* @param \WP_Post|null $post Post object.
* @param \WP_Block_Template|null $template Template object.
* @param bool $convertPresets Convert presets to valid CSS values.
* @return array{
* spacing: array{
* blockGap: string,
* padding: array{bottom: string, left: string, right: string, top: string}
* },
* color: array{
* background: string
* },
* typography: array{
* fontFamily: string
* }
* }
*/
public function getStyles($post = null, $template = null): array {
$themeStyles = $this->getTheme()->get_data()['styles'];
/**
* Get styles for the e-mail.
*
* @param \WP_Post|null $post Post object.
* @param \WP_Block_Template|null $template Template object.
* @param bool $convertPresets Convert presets to valid CSS values.
* @return array{
* spacing: array{
* blockGap: string,
* padding: array{bottom: string, left: string, right: string, top: string}
* },
* color: array{
* background: string
* },
* typography: array{
* fontFamily: string
* }
* }
*/
public function getStyles( $post = null, $template = null ): array {
$themeStyles = $this->getTheme()->get_data()['styles'];
// Replace template styles.
if ($template && $template->wp_id) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$templateTheme = (array)get_post_meta($template->wp_id, 'mailpoet_email_theme', true); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$templateStyles = (array)($templateTheme['styles'] ?? []);
$themeStyles = array_replace_recursive($themeStyles, $templateStyles);
}
// Replace template styles.
if ( $template && $template->wp_id ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$templateTheme = (array) get_post_meta( $template->wp_id, 'mailpoet_email_theme', true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$templateStyles = (array) ( $templateTheme['styles'] ?? array() );
$themeStyles = array_replace_recursive( $themeStyles, $templateStyles );
}
// Extract preset variables
$themeStyles = $this->recursiveExtractPresetVariables($themeStyles);
// Extract preset variables
$themeStyles = $this->recursiveExtractPresetVariables( $themeStyles );
// Replace preset values.
$variables = $this->getVariablesValuesMap();
$presets = [];
// Replace preset values.
$variables = $this->getVariablesValuesMap();
$presets = array();
foreach ($variables as $varName => $varValue) {
$varPattern = '/var\(' . preg_quote($varName, '/') . '\)/i';
$presets[$varPattern] = $varValue;
}
foreach ( $variables as $varName => $varValue ) {
$varPattern = '/var\(' . preg_quote( $varName, '/' ) . '\)/i';
$presets[ $varPattern ] = $varValue;
}
$themeStyles = $this->recursiveReplacePresets($themeStyles, $presets);
$themeStyles = $this->recursiveReplacePresets( $themeStyles, $presets );
return $themeStyles;
}
return $themeStyles;
}
public function getSettings(): array {
$emailEditorThemeSettings = $this->getTheme()->get_settings();
$siteThemeSettings = WP_Theme_JSON_Resolver::get_theme_data()->get_settings();
$emailEditorThemeSettings['color']['palette']['theme'] = [];
if (isset($siteThemeSettings['color']['palette']['theme'])) {
$emailEditorThemeSettings['color']['palette']['theme'] = $siteThemeSettings['color']['palette']['theme'];
}
return $emailEditorThemeSettings;
}
public function getSettings(): array {
$emailEditorThemeSettings = $this->getTheme()->get_settings();
$siteThemeSettings = WP_Theme_JSON_Resolver::get_theme_data()->get_settings();
$emailEditorThemeSettings['color']['palette']['theme'] = array();
if ( isset( $siteThemeSettings['color']['palette']['theme'] ) ) {
$emailEditorThemeSettings['color']['palette']['theme'] = $siteThemeSettings['color']['palette']['theme'];
}
return $emailEditorThemeSettings;
}
public function getLayoutSettings(): array {
return $this->getTheme()->get_settings()['layout'];
}
public function getLayoutSettings(): array {
return $this->getTheme()->get_settings()['layout'];
}
public function getStylesheetFromContext($context, $options = []): string {
return function_exists('gutenberg_style_engine_get_stylesheet_from_context') ? gutenberg_style_engine_get_stylesheet_from_context($context, $options) : wp_style_engine_get_stylesheet_from_context($context, $options);
}
public function getStylesheetFromContext( $context, $options = array() ): string {
return function_exists( 'gutenberg_style_engine_get_stylesheet_from_context' ) ? gutenberg_style_engine_get_stylesheet_from_context( $context, $options ) : wp_style_engine_get_stylesheet_from_context( $context, $options );
}
public function getStylesheetForRendering($post = null, $template = null): string {
$emailThemeSettings = $this->getSettings();
public function getStylesheetForRendering( $post = null, $template = null ): string {
$emailThemeSettings = $this->getSettings();
$cssPresets = '';
// Font family classes
foreach ($emailThemeSettings['typography']['fontFamilies']['default'] as $fontFamily) {
$cssPresets .= ".has-{$fontFamily['slug']}-font-family { font-family: {$fontFamily['fontFamily']}; } \n";
}
// Font size classes
foreach ($emailThemeSettings['typography']['fontSizes']['default'] as $fontSize) {
$cssPresets .= ".has-{$fontSize['slug']}-font-size { font-size: {$fontSize['size']}; } \n";
}
// Color palette classes
$colorDefinitions = array_merge($emailThemeSettings['color']['palette']['theme'], $emailThemeSettings['color']['palette']['default']);
foreach ($colorDefinitions as $color) {
$cssPresets .= ".has-{$color['slug']}-color { color: {$color['color']}; } \n";
$cssPresets .= ".has-{$color['slug']}-background-color { background-color: {$color['color']}; } \n";
$cssPresets .= ".has-{$color['slug']}-border-color { border-color: {$color['color']}; } \n";
}
$cssPresets = '';
// Font family classes
foreach ( $emailThemeSettings['typography']['fontFamilies']['default'] as $fontFamily ) {
$cssPresets .= ".has-{$fontFamily['slug']}-font-family { font-family: {$fontFamily['fontFamily']}; } \n";
}
// Font size classes
foreach ( $emailThemeSettings['typography']['fontSizes']['default'] as $fontSize ) {
$cssPresets .= ".has-{$fontSize['slug']}-font-size { font-size: {$fontSize['size']}; } \n";
}
// Color palette classes
$colorDefinitions = array_merge( $emailThemeSettings['color']['palette']['theme'], $emailThemeSettings['color']['palette']['default'] );
foreach ( $colorDefinitions as $color ) {
$cssPresets .= ".has-{$color['slug']}-color { color: {$color['color']}; } \n";
$cssPresets .= ".has-{$color['slug']}-background-color { background-color: {$color['color']}; } \n";
$cssPresets .= ".has-{$color['slug']}-border-color { border-color: {$color['color']}; } \n";
}
// Block specific styles
$cssBlocks = '';
$blocks = $this->getTheme()->get_styles_block_nodes();
foreach ($blocks as $blockMetadata) {
$cssBlocks .= $this->getTheme()->get_styles_for_block($blockMetadata);
}
// Block specific styles
$cssBlocks = '';
$blocks = $this->getTheme()->get_styles_block_nodes();
foreach ( $blocks as $blockMetadata ) {
$cssBlocks .= $this->getTheme()->get_styles_for_block( $blockMetadata );
}
// Element specific styles
$elementsStyles = $this->getTheme()->get_raw_data()['styles']['elements'] ?? [];
// Element specific styles
$elementsStyles = $this->getTheme()->get_raw_data()['styles']['elements'] ?? array();
// Because the section styles is not a part of the output the `get_styles_block_nodes` method, we need to get it separately
if ($template && $template->wp_id) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$templateTheme = (array)get_post_meta($template->wp_id, 'mailpoet_email_theme', true); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$templateStyles = (array)($templateTheme['styles'] ?? []);
$templateElements = $templateStyles['elements'] ?? [];
$elementsStyles = array_replace_recursive((array)$elementsStyles, (array)$templateElements);
}
// Because the section styles is not a part of the output the `get_styles_block_nodes` method, we need to get it separately
if ( $template && $template->wp_id ) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$templateTheme = (array) get_post_meta( $template->wp_id, 'mailpoet_email_theme', true ); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$templateStyles = (array) ( $templateTheme['styles'] ?? array() );
$templateElements = $templateStyles['elements'] ?? array();
$elementsStyles = array_replace_recursive( (array) $elementsStyles, (array) $templateElements );
}
if ($post) {
$postTheme = (array)get_post_meta($post->ID, 'mailpoet_email_theme', true);
$postStyles = (array)($postTheme['styles'] ?? []);
$postElements = $postStyles['elements'] ?? [];
$elementsStyles = array_replace_recursive((array)$elementsStyles, (array)$postElements);
}
if ( $post ) {
$postTheme = (array) get_post_meta( $post->ID, 'mailpoet_email_theme', true );
$postStyles = (array) ( $postTheme['styles'] ?? array() );
$postElements = $postStyles['elements'] ?? array();
$elementsStyles = array_replace_recursive( (array) $elementsStyles, (array) $postElements );
}
$cssElements = '';
foreach ($elementsStyles as $key => $elementsStyle) {
$selector = $key;
$cssElements = '';
foreach ( $elementsStyles as $key => $elementsStyle ) {
$selector = $key;
if ($key === 'button') {
$selector = '.wp-block-button';
$cssElements .= wp_style_engine_get_styles($elementsStyle, ['selector' => '.wp-block-button'])['css'];
// Add color to link element.
$cssElements .= wp_style_engine_get_styles(['color' => ['text' => $elementsStyle['color']['text'] ?? '']], ['selector' => '.wp-block-button a'])['css'];
continue;
}
if ( $key === 'button' ) {
$selector = '.wp-block-button';
$cssElements .= wp_style_engine_get_styles( $elementsStyle, array( 'selector' => '.wp-block-button' ) )['css'];
// Add color to link element.
$cssElements .= wp_style_engine_get_styles( array( 'color' => array( 'text' => $elementsStyle['color']['text'] ?? '' ) ), array( 'selector' => '.wp-block-button a' ) )['css'];
continue;
}
switch ($key) {
case 'heading':
$selector = 'h1, h2, h3, h4, h5, h6';
break;
case 'link':
$selector = 'a:not(.button-link)';
break;
}
switch ( $key ) {
case 'heading':
$selector = 'h1, h2, h3, h4, h5, h6';
break;
case 'link':
$selector = 'a:not(.button-link)';
break;
}
$cssElements .= wp_style_engine_get_styles($elementsStyle, ['selector' => $selector])['css'];
}
$cssElements .= wp_style_engine_get_styles( $elementsStyle, array( 'selector' => $selector ) )['css'];
}
$result = $cssPresets . $cssBlocks . $cssElements;
// Because font-size can by defined by the clamp() function that is not supported in the e-mail clients, we need to replace it to the value.
// Regular expression to match clamp() function and capture its max value
$pattern = '/clamp\([^,]+,\s*[^,]+,\s*([^)]+)\)/';
// Replace clamp() with its maximum value
$result = (string)preg_replace($pattern, '$1', $result);
return $result;
}
$result = $cssPresets . $cssBlocks . $cssElements;
// Because font-size can by defined by the clamp() function that is not supported in the e-mail clients, we need to replace it to the value.
// Regular expression to match clamp() function and capture its max value
$pattern = '/clamp\([^,]+,\s*[^,]+,\s*([^)]+)\)/';
// Replace clamp() with its maximum value
$result = (string) preg_replace( $pattern, '$1', $result );
return $result;
}
public function translateSlugToFontSize(string $fontSize): string {
$settings = $this->getSettings();
foreach ($settings['typography']['fontSizes']['default'] as $fontSizeDefinition) {
if ($fontSizeDefinition['slug'] === $fontSize) {
return $fontSizeDefinition['size'];
}
}
return $fontSize;
}
public function translateSlugToFontSize( string $fontSize ): string {
$settings = $this->getSettings();
foreach ( $settings['typography']['fontSizes']['default'] as $fontSizeDefinition ) {
if ( $fontSizeDefinition['slug'] === $fontSize ) {
return $fontSizeDefinition['size'];
}
}
return $fontSize;
}
public function translateSlugToColor(string $colorSlug): string {
$settings = $this->getSettings();
$colorDefinitions = array_merge($settings['color']['palette']['theme'], $settings['color']['palette']['default']);
foreach ($colorDefinitions as $colorDefinition) {
if ($colorDefinition['slug'] === $colorSlug) {
return strtolower($colorDefinition['color']);
}
}
return $colorSlug;
}
public function translateSlugToColor( string $colorSlug ): string {
$settings = $this->getSettings();
$colorDefinitions = array_merge( $settings['color']['palette']['theme'], $settings['color']['palette']['default'] );
foreach ( $colorDefinitions as $colorDefinition ) {
if ( $colorDefinition['slug'] === $colorSlug ) {
return strtolower( $colorDefinition['color'] );
}
}
return $colorSlug;
}
public function getVariablesValuesMap(): array {
$variablesCss = $this->getTheme()->get_stylesheet(['variables']);
$map = [];
// Regular expression to match CSS variable definitions
$pattern = '/--(.*?):\s*(.*?);/';
public function getVariablesValuesMap(): array {
$variablesCss = $this->getTheme()->get_stylesheet( array( 'variables' ) );
$map = array();
// Regular expression to match CSS variable definitions
$pattern = '/--(.*?):\s*(.*?);/';
if (preg_match_all($pattern, $variablesCss, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
// '--' . $match[1] is the variable name, $match[2] is the variable value
$map['--' . $match[1]] = $match[2];
}
}
if ( preg_match_all( $pattern, $variablesCss, $matches, PREG_SET_ORDER ) ) {
foreach ( $matches as $match ) {
// '--' . $match[1] is the variable name, $match[2] is the variable value
$map[ '--' . $match[1] ] = $match[2];
}
}
return $map;
}
return $map;
}
}

View File

@ -1,28 +1,28 @@
/*
* Styles for the email editor.
* Styles for the email editor.
*/
/*
* Flex layout used for buttons block for email editor.
*/
.is-layout-email-flex {
flex-wrap: nowrap;
flex-wrap: nowrap;
}
:where(body .is-layout-flex) {
gap: var(--wp--style--block-gap, 16px);
gap: var(--wp--style--block-gap, 16px);
}
.is-mobile-preview .is-layout-email-flex {
display: block;
display: block;
}
.is-mobile-preview .is-layout-email-flex .block-editor-block-list__block {
padding: 5px 0;
width: 100%;
padding: 5px 0;
width: 100%;
}
.is-mobile-preview .is-layout-email-flex .wp-block-button__link {
width: 100%;
width: 100%;
}
/*
@ -30,61 +30,61 @@
* This is needed because we disable layout for core/group, core/column and core/columns blocks, and .is-layout-flex is not applied.
*/
.wp-block-columns:not(.is-not-stacked-on-mobile)
> .wp-block-column
> .wp-block:first-child,
> .wp-block-column
> .wp-block:first-child,
.wp-block-group > .wp-block:first-child {
margin-top: 0;
margin-top: 0;
}
.wp-block-columns:not(.is-not-stacked-on-mobile) > .wp-block-column > .wp-block,
.wp-block-group > .wp-block {
margin-bottom: var(--wp--style--block-gap, 16px);
margin-top: var(--wp--style--block-gap, 16px);
margin-bottom: var(--wp--style--block-gap, 16px);
margin-top: var(--wp--style--block-gap, 16px);
}
.wp-block-columns:not(.is-not-stacked-on-mobile)
> .wp-block-column
> .wp-block:last-child,
> .wp-block-column
> .wp-block:last-child,
.wp-block-group > .wp-block:last-child {
margin-bottom: 0;
margin-bottom: 0;
}
/*
* Use box sizing border box for columns that have defined a width (they have flex-basis set).
*/
.wp-block-columns:not(.is-not-stacked-on-mobile)
> .wp-block-column[style*='flex-basis'] {
box-sizing: border-box;
> .wp-block-column[style*='flex-basis'] {
box-sizing: border-box;
}
/*
* For the WYSIWYG experience we don't want to display any margins between blocks in the editor
*/
.wp-block {
clear: both; // for ensuring that floated elements (images) are cleared
clear: both; // for ensuring that floated elements (images) are cleared
}
/*
* Image block enhancements
*/
.wp-block-image figcaption {
/* Resetting the margin for images in the editor to avoid unexpected spacing */
margin: 0;
/* Resetting the margin for images in the editor to avoid unexpected spacing */
margin: 0;
}
.wp-block-image.alignleft,
.wp-block-image.alignright {
margin-inline: 0 0;
text-align: center;
margin-inline: 0 0;
text-align: center;
}
.wp-block-image.aligncenter {
margin-left: auto;
margin-right: auto;
margin-left: auto;
margin-right: auto;
}
.wp-block-image.alignright {
margin-left: auto;
margin-left: auto;
}
/*
@ -95,31 +95,31 @@ ul,
ol,
ul.has-background,
ol.has-background {
padding-left: 40px;
padding-left: 40px;
}
/*
* Override default button border radius which is set in core to 9999px
*/
.wp-block-button__link {
border-radius: 0;
border-radius: 0;
}
/*
* Mobile preview fixes
*/
.is-mobile-preview figure > div {
height: auto !important;
width: auto !important;
height: auto !important;
width: auto !important;
}
.is-mobile-preview .wp-block-columns {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
}
.is-mobile-preview .wp-block-column {
box-sizing: border-box;
/* override flex-basis set in style attribute to fix the height of the column in mobile preview. Blocks overriding is as a part of style.css in blocks-library */
flex-basis: auto !important;
box-sizing: border-box;
/* override flex-basis set in style attribute to fix the height of the column in mobile preview. Blocks overriding is as a part of style.css in blocks-library */
flex-basis: auto !important;
}

View File

@ -11,5 +11,5 @@ h3.has-background,
h4.has-background,
h5.has-background,
h6.has-background {
padding: 16px 24px;
padding: 16px 24px;
}

View File

@ -10,56 +10,59 @@ use WP_Style_Engine;
* Shared functionality for block renderers.
*/
abstract class Abstract_Block_Renderer implements Block_Renderer {
/**
* Wrapper for wp_style_engine_get_styles which ensures all values are returned.
*
* @param array $block_styles Array of block styles.
* @param bool $skip_convert_vars If true, --wp_preset--spacing--x type values will be left in the original var:preset:spacing:x format.
* @return array
*/
protected function getStylesFromBlock(array $block_styles, $skip_convert_vars = false) {
$styles = wp_style_engine_get_styles($block_styles, ['convert_vars_to_classnames' => $skip_convert_vars]);
return wp_parse_args($styles, [
'css' => '',
'declarations' => [],
'classnames' => '',
]);
}
/**
* Wrapper for wp_style_engine_get_styles which ensures all values are returned.
*
* @param array $block_styles Array of block styles.
* @param bool $skip_convert_vars If true, --wp_preset--spacing--x type values will be left in the original var:preset:spacing:x format.
* @return array
*/
protected function getStylesFromBlock( array $block_styles, $skip_convert_vars = false ) {
$styles = wp_style_engine_get_styles( $block_styles, array( 'convert_vars_to_classnames' => $skip_convert_vars ) );
return wp_parse_args(
$styles,
array(
'css' => '',
'declarations' => array(),
'classnames' => '',
)
);
}
/**
* Compile objects containing CSS properties to a string.
*
* @param array ...$styles Style arrays to compile.
* @return string
*/
protected function compileCss(...$styles): string {
return WP_Style_Engine::compile_css(array_merge(...$styles), '');
}
/**
* Compile objects containing CSS properties to a string.
*
* @param array ...$styles Style arrays to compile.
* @return string
*/
protected function compileCss( ...$styles ): string {
return WP_Style_Engine::compile_css( array_merge( ...$styles ), '' );
}
protected function addSpacer($content, $emailAttrs): string {
$gapStyle = WP_Style_Engine::compile_css(array_intersect_key($emailAttrs, array_flip(['margin-top'])), '');
$paddingStyle = WP_Style_Engine::compile_css(array_intersect_key($emailAttrs, array_flip(['padding-left', 'padding-right'])), '');
protected function addSpacer( $content, $emailAttrs ): string {
$gapStyle = WP_Style_Engine::compile_css( array_intersect_key( $emailAttrs, array_flip( array( 'margin-top' ) ) ), '' );
$paddingStyle = WP_Style_Engine::compile_css( array_intersect_key( $emailAttrs, array_flip( array( 'padding-left', 'padding-right' ) ) ), '' );
if (!$gapStyle && !$paddingStyle) {
return $content;
}
if ( ! $gapStyle && ! $paddingStyle ) {
return $content;
}
return sprintf(
'<!--[if mso | IE]><table align="left" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%%" style="%2$s"><tr><td style="%3$s"><![endif]-->
return sprintf(
'<!--[if mso | IE]><table align="left" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%%" style="%2$s"><tr><td style="%3$s"><![endif]-->
<div class="email-block-layout" style="%2$s %3$s">%1$s</div>
<!--[if mso | IE]></td></tr></table><![endif]-->',
$content,
esc_attr($gapStyle),
esc_attr($paddingStyle)
);
}
$content,
esc_attr( $gapStyle ),
esc_attr( $paddingStyle )
);
}
public function render(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
return $this->addSpacer(
$this->renderContent($blockContent, $parsedBlock, $settingsController),
$parsedBlock['email_attrs'] ?? []
);
}
public function render( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
return $this->addSpacer(
$this->renderContent( $blockContent, $parsedBlock, $settingsController ),
$parsedBlock['email_attrs'] ?? array()
);
}
abstract protected function renderContent(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string;
abstract protected function renderContent( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string;
}

View File

@ -7,93 +7,107 @@ use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
/**
* Renders a button block.
*
* @see https://www.activecampaign.com/blog/email-buttons
* @see https://documentation.mjml.io/#mj-button
*/
class Button extends Abstract_Block_Renderer {
private function getWrapperStyles(array $blockStyles) {
$properties = ['border', 'color', 'typography', 'spacing'];
$styles = $this->getStylesFromBlock(array_intersect_key($blockStyles, array_flip($properties)));
return (object)[
'css' => $this->compileCss($styles['declarations'], ['word-break' => 'break-word', 'display' => 'block']),
'classname' => $styles['classnames'],
];
}
private function getWrapperStyles( array $blockStyles ) {
$properties = array( 'border', 'color', 'typography', 'spacing' );
$styles = $this->getStylesFromBlock( array_intersect_key( $blockStyles, array_flip( $properties ) ) );
return (object) array(
'css' => $this->compileCss(
$styles['declarations'],
array(
'word-break' => 'break-word',
'display' => 'block',
)
),
'classname' => $styles['classnames'],
);
}
private function getLinkStyles(array $blockStyles) {
$styles = $this->getStylesFromBlock([
'color' => [
'text' => $blockStyles['color']['text'] ?? '',
],
'typography' => $blockStyles['typography'] ?? [],
]);
return (object)[
'css' => $this->compileCss($styles['declarations'], ['display' => 'block']),
'classname' => $styles['classnames'],
];
}
private function getLinkStyles( array $blockStyles ) {
$styles = $this->getStylesFromBlock(
array(
'color' => array(
'text' => $blockStyles['color']['text'] ?? '',
),
'typography' => $blockStyles['typography'] ?? array(),
)
);
return (object) array(
'css' => $this->compileCss( $styles['declarations'], array( 'display' => 'block' ) ),
'classname' => $styles['classnames'],
);
}
public function render(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
return $this->renderContent($blockContent, $parsedBlock, $settingsController);
}
public function render( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
return $this->renderContent( $blockContent, $parsedBlock, $settingsController );
}
protected function renderContent($blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
if (empty($parsedBlock['innerHTML'])) {
return '';
}
protected function renderContent( $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
if ( empty( $parsedBlock['innerHTML'] ) ) {
return '';
}
$domHelper = new Dom_Document_Helper($parsedBlock['innerHTML']);
$blockClassname = $domHelper->getAttributeValueByTagName('div', 'class') ?? '';
$buttonLink = $domHelper->findElement('a');
$domHelper = new Dom_Document_Helper( $parsedBlock['innerHTML'] );
$blockClassname = $domHelper->getAttributeValueByTagName( 'div', 'class' ) ?? '';
$buttonLink = $domHelper->findElement( 'a' );
if (!$buttonLink) {
return '';
}
if ( ! $buttonLink ) {
return '';
}
$buttonText = $domHelper->getElementInnerHTML($buttonLink) ?: '';
$buttonUrl = $buttonLink->getAttribute('href') ?: '#';
$buttonText = $domHelper->getElementInnerHTML( $buttonLink ) ?: '';
$buttonUrl = $buttonLink->getAttribute( 'href' ) ?: '#';
$blockAttributes = wp_parse_args($parsedBlock['attrs'] ?? [], [
'width' => '',
'style' => [],
'textAlign' => 'center',
'backgroundColor' => '',
'textColor' => '',
]);
$blockAttributes = wp_parse_args(
$parsedBlock['attrs'] ?? array(),
array(
'width' => '',
'style' => array(),
'textAlign' => 'center',
'backgroundColor' => '',
'textColor' => '',
)
);
$blockStyles = array_replace_recursive(
[
'color' => array_filter([
'background' => $blockAttributes['backgroundColor'] ? $settingsController->translateSlugToColor($blockAttributes['backgroundColor']) : null,
'text' => $blockAttributes['textColor'] ? $settingsController->translateSlugToColor($blockAttributes['textColor']) : null,
]),
],
$blockAttributes['style'] ?? []
);
$blockStyles = array_replace_recursive(
array(
'color' => array_filter(
array(
'background' => $blockAttributes['backgroundColor'] ? $settingsController->translateSlugToColor( $blockAttributes['backgroundColor'] ) : null,
'text' => $blockAttributes['textColor'] ? $settingsController->translateSlugToColor( $blockAttributes['textColor'] ) : null,
)
),
),
$blockAttributes['style'] ?? array()
);
if (!empty($blockStyles['border']) && empty($blockStyles['border']['style'])) {
$blockStyles['border']['style'] = 'solid';
}
if ( ! empty( $blockStyles['border'] ) && empty( $blockStyles['border']['style'] ) ) {
$blockStyles['border']['style'] = 'solid';
}
$wrapperStyles = $this->getWrapperStyles($blockStyles);
$linkStyles = $this->getLinkStyles($blockStyles);
$wrapperStyles = $this->getWrapperStyles( $blockStyles );
$linkStyles = $this->getLinkStyles( $blockStyles );
return sprintf(
'<table border="0" cellspacing="0" cellpadding="0" role="presentation" style="width:%1$s;">
return sprintf(
'<table border="0" cellspacing="0" cellpadding="0" role="presentation" style="width:%1$s;">
<tr>
<td align="%2$s" valign="middle" role="presentation" class="%3$s" style="%4$s">
<a class="button-link %5$s" style="%6$s" href="%7$s" target="_blank">%8$s</a>
</td>
</tr>
</table>',
esc_attr($blockAttributes['width'] ? '100%' : 'auto'),
esc_attr($blockAttributes['textAlign']),
esc_attr($wrapperStyles->classname . ' ' . $blockClassname),
esc_attr($wrapperStyles->css),
esc_attr($linkStyles->classname),
esc_attr($linkStyles->css),
esc_url($buttonUrl),
$buttonText,
);
}
esc_attr( $blockAttributes['width'] ? '100%' : 'auto' ),
esc_attr( $blockAttributes['textAlign'] ),
esc_attr( $wrapperStyles->classname . ' ' . $blockClassname ),
esc_attr( $wrapperStyles->css ),
esc_attr( $linkStyles->classname ),
esc_attr( $linkStyles->css ),
esc_url( $buttonUrl ),
$buttonText,
);
}
}

View File

@ -6,22 +6,22 @@ use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Layout\Flex_Layout_Rend
use MailPoet\EmailEditor\Engine\Settings_Controller;
class Buttons extends Abstract_Block_Renderer {
/** @var Flex_Layout_Renderer */
private $flexLayoutRenderer;
/** @var Flex_Layout_Renderer */
private $flexLayoutRenderer;
public function __construct(
Flex_Layout_Renderer $flexLayoutRenderer
) {
$this->flexLayoutRenderer = $flexLayoutRenderer;
}
public function __construct(
Flex_Layout_Renderer $flexLayoutRenderer
) {
$this->flexLayoutRenderer = $flexLayoutRenderer;
}
protected function renderContent($blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
// Ignore font size set on the buttons block
// We rely on TypographyPreprocessor to set the font size on the buttons
// Rendering font size on the wrapper causes unwanted whitespace below the buttons
if (isset($parsedBlock['attrs']['style']['typography']['fontSize'])) {
unset($parsedBlock['attrs']['style']['typography']['fontSize']);
}
return $this->flexLayoutRenderer->renderInnerBlocksInLayout($parsedBlock, $settingsController);
}
protected function renderContent( $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
// Ignore font size set on the buttons block
// We rely on TypographyPreprocessor to set the font size on the buttons
// Rendering font size on the wrapper causes unwanted whitespace below the buttons
if ( isset( $parsedBlock['attrs']['style']['typography']['fontSize'] ) ) {
unset( $parsedBlock['attrs']['style']['typography']['fontSize'] );
}
return $this->flexLayoutRenderer->renderInnerBlocksInLayout( $parsedBlock, $settingsController );
}
}

View File

@ -7,79 +7,87 @@ use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
use WP_Style_Engine;
class Column extends Abstract_Block_Renderer {
/**
* Override this method to disable spacing (block gap) for columns.
* Spacing is applied on wrapping columns block. Columns are rendered side by side so no spacer is needed.
*/
protected function addSpacer($content, $emailAttrs): string {
return $content;
}
/**
* Override this method to disable spacing (block gap) for columns.
* Spacing is applied on wrapping columns block. Columns are rendered side by side so no spacer is needed.
*/
protected function addSpacer( $content, $emailAttrs ): string {
return $content;
}
protected function renderContent(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$content = '';
foreach ($parsedBlock['innerBlocks'] ?? [] as $block) {
$content .= render_block($block);
}
protected function renderContent( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$content = '';
foreach ( $parsedBlock['innerBlocks'] ?? array() as $block ) {
$content .= render_block( $block );
}
return str_replace(
'{column_content}',
$content,
$this->getBlockWrapper($blockContent, $parsedBlock, $settingsController)
);
}
return str_replace(
'{column_content}',
$content,
$this->getBlockWrapper( $blockContent, $parsedBlock, $settingsController )
);
}
/**
* Based on MJML <mj-column>
*/
private function getBlockWrapper(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$originalWrapperClassname = (new Dom_Document_Helper($blockContent))->getAttributeValueByTagName('div', 'class') ?? '';
$block_attributes = wp_parse_args($parsedBlock['attrs'] ?? [], [
'verticalAlignment' => 'stretch',
'width' => $settingsController->getLayoutWidthWithoutPadding(),
'style' => [],
]);
/**
* Based on MJML <mj-column>
*/
private function getBlockWrapper( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$originalWrapperClassname = ( new Dom_Document_Helper( $blockContent ) )->getAttributeValueByTagName( 'div', 'class' ) ?? '';
$block_attributes = wp_parse_args(
$parsedBlock['attrs'] ?? array(),
array(
'verticalAlignment' => 'stretch',
'width' => $settingsController->getLayoutWidthWithoutPadding(),
'style' => array(),
)
);
// The default column alignment is `stretch to fill` which means that we need to set the background color to the main cell
// to create a feeling of a stretched column. This also needs to apply to CSS classnames which can also apply styles.
$isStretched = empty($block_attributes['verticalAlignment']) || $block_attributes['verticalAlignment'] === 'stretch';
// The default column alignment is `stretch to fill` which means that we need to set the background color to the main cell
// to create a feeling of a stretched column. This also needs to apply to CSS classnames which can also apply styles.
$isStretched = empty( $block_attributes['verticalAlignment'] ) || $block_attributes['verticalAlignment'] === 'stretch';
$paddingCSS = $this->getStylesFromBlock(['spacing' => ['padding' => $block_attributes['style']['spacing']['padding'] ?? []]])['css'];
$cellStyles = $this->getStylesFromBlock([
'color' => $block_attributes['style']['color'] ?? [],
'background' => $block_attributes['style']['background'] ?? [],
])['declarations'];
$paddingCSS = $this->getStylesFromBlock( array( 'spacing' => array( 'padding' => $block_attributes['style']['spacing']['padding'] ?? array() ) ) )['css'];
$cellStyles = $this->getStylesFromBlock(
array(
'color' => $block_attributes['style']['color'] ?? array(),
'background' => $block_attributes['style']['background'] ?? array(),
)
)['declarations'];
$borderStyles = $this->getStylesFromBlock(['border' => $block_attributes['style']['border'] ?? []])['declarations'];
$borderStyles = $this->getStylesFromBlock( array( 'border' => $block_attributes['style']['border'] ?? array() ) )['declarations'];
if (!empty($borderStyles)) {
$cellStyles = array_merge($cellStyles, ['border-style' => 'solid'], $borderStyles);
}
if ( ! empty( $borderStyles ) ) {
$cellStyles = array_merge( $cellStyles, array( 'border-style' => 'solid' ), $borderStyles );
}
if (!empty($cellStyles['background-image']) && empty($cellStyles['background-size'])) {
$cellStyles['background-size'] = 'cover';
}
if ( ! empty( $cellStyles['background-image'] ) && empty( $cellStyles['background-size'] ) ) {
$cellStyles['background-size'] = 'cover';
}
$wrapperClassname = 'block wp-block-column email-block-column';
$contentClassname = 'email-block-column-content';
$wrapperCSS = WP_Style_Engine::compile_css([
'vertical-align' => $isStretched ? 'top' : $block_attributes['verticalAlignment'],
], '');
$contentCSS = 'vertical-align: top;';
$wrapperClassname = 'block wp-block-column email-block-column';
$contentClassname = 'email-block-column-content';
$wrapperCSS = WP_Style_Engine::compile_css(
array(
'vertical-align' => $isStretched ? 'top' : $block_attributes['verticalAlignment'],
),
''
);
$contentCSS = 'vertical-align: top;';
if ($isStretched) {
$wrapperClassname .= ' ' . $originalWrapperClassname;
$wrapperCSS .= ' ' . WP_Style_Engine::compile_css($cellStyles, '');
} else {
$contentClassname .= ' ' . $originalWrapperClassname;
$contentCSS .= ' ' . WP_Style_Engine::compile_css($cellStyles, '');
}
if ( $isStretched ) {
$wrapperClassname .= ' ' . $originalWrapperClassname;
$wrapperCSS .= ' ' . WP_Style_Engine::compile_css( $cellStyles, '' );
} else {
$contentClassname .= ' ' . $originalWrapperClassname;
$contentCSS .= ' ' . WP_Style_Engine::compile_css( $cellStyles, '' );
}
return '
<td class="' . esc_attr($wrapperClassname) . '" style="' . esc_attr($wrapperCSS) . '" width="' . esc_attr($block_attributes['width']) . '">
<table class="' . esc_attr($contentClassname) . '" style="' . esc_attr($contentCSS) . '" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
return '
<td class="' . esc_attr( $wrapperClassname ) . '" style="' . esc_attr( $wrapperCSS ) . '" width="' . esc_attr( $block_attributes['width'] ) . '">
<table class="' . esc_attr( $contentClassname ) . '" style="' . esc_attr( $contentCSS ) . '" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td align="left" style="text-align:left;' . esc_attr($paddingCSS) . '">
<td align="left" style="text-align:left;' . esc_attr( $paddingCSS ) . '">
{column_content}
</td>
</tr>
@ -87,5 +95,5 @@ class Column extends Abstract_Block_Renderer {
</table>
</td>
';
}
}
}

View File

@ -8,68 +8,75 @@ use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
use WP_Style_Engine;
class Columns extends Abstract_Block_Renderer {
protected function renderContent(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$content = '';
foreach ($parsedBlock['innerBlocks'] ?? [] as $block) {
$content .= render_block($block);
}
protected function renderContent( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$content = '';
foreach ( $parsedBlock['innerBlocks'] ?? array() as $block ) {
$content .= render_block( $block );
}
return str_replace(
'{columns_content}',
$content,
$this->getBlockWrapper($blockContent, $parsedBlock, $settingsController)
);
}
return str_replace(
'{columns_content}',
$content,
$this->getBlockWrapper( $blockContent, $parsedBlock, $settingsController )
);
}
/**
* Based on MJML <mj-section>
*/
private function getBlockWrapper(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$originalWrapperClassname = (new Dom_Document_Helper($blockContent))->getAttributeValueByTagName('div', 'class') ?? '';
$block_attributes = wp_parse_args($parsedBlock['attrs'] ?? [], [
'align' => null,
'width' => $settingsController->getLayoutWidthWithoutPadding(),
'style' => [],
]);
/**
* Based on MJML <mj-section>
*/
private function getBlockWrapper( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$originalWrapperClassname = ( new Dom_Document_Helper( $blockContent ) )->getAttributeValueByTagName( 'div', 'class' ) ?? '';
$block_attributes = wp_parse_args(
$parsedBlock['attrs'] ?? array(),
array(
'align' => null,
'width' => $settingsController->getLayoutWidthWithoutPadding(),
'style' => array(),
)
);
$columnsStyles = $this->getStylesFromBlock([
'spacing' => [ 'padding' => $block_attributes['style']['spacing']['padding'] ?? [] ],
'color' => $block_attributes['style']['color'] ?? [],
'background' => $block_attributes['style']['background'] ?? [],
])['declarations'];
$columnsStyles = $this->getStylesFromBlock(
array(
'spacing' => array( 'padding' => $block_attributes['style']['spacing']['padding'] ?? array() ),
'color' => $block_attributes['style']['color'] ?? array(),
'background' => $block_attributes['style']['background'] ?? array(),
)
)['declarations'];
$borderStyles = $this->getStylesFromBlock(['border' => $block_attributes['style']['border'] ?? []])['declarations'];
$borderStyles = $this->getStylesFromBlock( array( 'border' => $block_attributes['style']['border'] ?? array() ) )['declarations'];
if (!empty($borderStyles)) {
$columnsStyles = array_merge($columnsStyles, ['border-style' => 'solid'], $borderStyles);
}
if ( ! empty( $borderStyles ) ) {
$columnsStyles = array_merge( $columnsStyles, array( 'border-style' => 'solid' ), $borderStyles );
}
if (empty($columnsStyles['background-size'])) {
$columnsStyles['background-size'] = 'cover';
}
if ( empty( $columnsStyles['background-size'] ) ) {
$columnsStyles['background-size'] = 'cover';
}
$renderedColumns = '<table class="' . esc_attr('email-block-columns ' . $originalWrapperClassname) . '" style="width:100%;border-collapse:separate;text-align:left;' . esc_attr(WP_Style_Engine::compile_css($columnsStyles, '')) . '" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation">
$renderedColumns = '<table class="' . esc_attr( 'email-block-columns ' . $originalWrapperClassname ) . '" style="width:100%;border-collapse:separate;text-align:left;' . esc_attr( WP_Style_Engine::compile_css( $columnsStyles, '' ) ) . '" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>{columns_content}</tr>
</tbody>
</table>';
// Margins are not supported well in outlook for tables, so wrap in another table.
$margins = $block_attributes['style']['spacing']['margin'] ?? [];
// Margins are not supported well in outlook for tables, so wrap in another table.
$margins = $block_attributes['style']['spacing']['margin'] ?? array();
if (!empty($margins)) {
$marginToPaddingStyles = $this->getStylesFromBlock([
'spacing' => [ 'margin' => $margins ],
])['css'];
$renderedColumns = '<table class="email-block-columns-wrapper" style="width:100%;border-collapse:separate;text-align:left;' . esc_attr($marginToPaddingStyles) . '" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation">
if ( ! empty( $margins ) ) {
$marginToPaddingStyles = $this->getStylesFromBlock(
array(
'spacing' => array( 'margin' => $margins ),
)
)['css'];
$renderedColumns = '<table class="email-block-columns-wrapper" style="width:100%;border-collapse:separate;text-align:left;' . esc_attr( $marginToPaddingStyles ) . '" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td>' . $renderedColumns . '</td>
</tr>
</tbody>
</table>';
}
}
return $renderedColumns;
}
return $renderedColumns;
}
}

View File

@ -14,7 +14,7 @@ use MailPoet\EmailEditor\Engine\Settings_Controller;
* We need to find a better abstraction/architecture for this.
*/
class Fallback extends Abstract_Block_Renderer {
protected function renderContent($blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
return $blockContent;
}
protected function renderContent( $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
return $blockContent;
}
}

View File

@ -8,55 +8,64 @@ use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
use WP_Style_Engine;
class Group extends Abstract_Block_Renderer {
protected function renderContent(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$content = '';
$innerBlocks = $parsedBlock['innerBlocks'] ?? [];
protected function renderContent( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$content = '';
$innerBlocks = $parsedBlock['innerBlocks'] ?? array();
foreach ($innerBlocks as $block) {
$content .= render_block($block);
}
foreach ( $innerBlocks as $block ) {
$content .= render_block( $block );
}
return str_replace(
'{group_content}',
$content,
$this->getBlockWrapper($blockContent, $parsedBlock, $settingsController)
);
}
return str_replace(
'{group_content}',
$content,
$this->getBlockWrapper( $blockContent, $parsedBlock, $settingsController )
);
}
private function getBlockWrapper(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$originalClassname = (new Dom_Document_Helper($blockContent))->getAttributeValueByTagName('div', 'class') ?? '';
$blockAttributes = wp_parse_args($parsedBlock['attrs'] ?? [], [
'style' => [],
'backgroundColor' => '',
'textColor' => '',
'borderColor' => '',
'layout' => [],
]);
private function getBlockWrapper( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$originalClassname = ( new Dom_Document_Helper( $blockContent ) )->getAttributeValueByTagName( 'div', 'class' ) ?? '';
$blockAttributes = wp_parse_args(
$parsedBlock['attrs'] ?? array(),
array(
'style' => array(),
'backgroundColor' => '',
'textColor' => '',
'borderColor' => '',
'layout' => array(),
)
);
// Layout, background, borders need to be on the outer table element.
$tableStyles = $this->getStylesFromBlock([
'color' => array_filter([
'background' => $blockAttributes['backgroundColor'] ? $settingsController->translateSlugToColor($blockAttributes['backgroundColor']) : null,
'text' => $blockAttributes['textColor'] ? $settingsController->translateSlugToColor($blockAttributes['textColor']) : null,
'border' => $blockAttributes['borderColor'] ? $settingsController->translateSlugToColor($blockAttributes['borderColor']) : null,
]),
'background' => $blockAttributes['style']['background'] ?? [],
'border' => $blockAttributes['style']['border'] ?? [],
'spacing' => [ 'padding' => $blockAttributes['style']['spacing']['margin'] ?? [] ],
])['declarations'];
$tableStyles['border-collapse'] = 'separate'; // Needed for the border radius to work.
// Layout, background, borders need to be on the outer table element.
$tableStyles = $this->getStylesFromBlock(
array(
'color' => array_filter(
array(
'background' => $blockAttributes['backgroundColor'] ? $settingsController->translateSlugToColor( $blockAttributes['backgroundColor'] ) : null,
'text' => $blockAttributes['textColor'] ? $settingsController->translateSlugToColor( $blockAttributes['textColor'] ) : null,
'border' => $blockAttributes['borderColor'] ? $settingsController->translateSlugToColor( $blockAttributes['borderColor'] ) : null,
)
),
'background' => $blockAttributes['style']['background'] ?? array(),
'border' => $blockAttributes['style']['border'] ?? array(),
'spacing' => array( 'padding' => $blockAttributes['style']['spacing']['margin'] ?? array() ),
)
)['declarations'];
$tableStyles['border-collapse'] = 'separate'; // Needed for the border radius to work.
// Padding properties need to be added to the table cell.
$cellStyles = $this->getStylesFromBlock([
'spacing' => [ 'padding' => $blockAttributes['style']['spacing']['padding'] ?? [] ],
])['declarations'];
// Padding properties need to be added to the table cell.
$cellStyles = $this->getStylesFromBlock(
array(
'spacing' => array( 'padding' => $blockAttributes['style']['spacing']['padding'] ?? array() ),
)
)['declarations'];
$tableStyles['background-size'] = empty($tableStyles['background-size']) ? 'cover' : $tableStyles['background-size'];
$justifyContent = $blockAttributes['layout']['justifyContent'] ?? 'center';
$width = $parsedBlock['email_attrs']['width'] ?? '100%';
$tableStyles['background-size'] = empty( $tableStyles['background-size'] ) ? 'cover' : $tableStyles['background-size'];
$justifyContent = $blockAttributes['layout']['justifyContent'] ?? 'center';
$width = $parsedBlock['email_attrs']['width'] ?? '100%';
return sprintf(
'<table class="email-block-group %3$s" style="%1$s" width="100%%" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation">
return sprintf(
'<table class="email-block-group %3$s" style="%1$s" width="100%%" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td class="email-block-group-content" style="%2$s" align="%4$s" width="%5$s">
@ -65,11 +74,11 @@ class Group extends Abstract_Block_Renderer {
</tr>
</tbody>
</table>',
esc_attr(WP_Style_Engine::compile_css($tableStyles, '')),
esc_attr(WP_Style_Engine::compile_css($cellStyles, '')),
esc_attr($originalClassname),
esc_attr($justifyContent),
esc_attr($width),
);
}
esc_attr( WP_Style_Engine::compile_css( $tableStyles, '' ) ),
esc_attr( WP_Style_Engine::compile_css( $cellStyles, '' ) ),
esc_attr( $originalClassname ),
esc_attr( $justifyContent ),
esc_attr( $width ),
);
}
}

View File

@ -6,190 +6,209 @@ use MailPoet\EmailEditor\Engine\Settings_Controller;
use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
class Image extends Abstract_Block_Renderer {
protected function renderContent($blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$parsedHtml = $this->parseBlockContent($blockContent);
protected function renderContent( $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$parsedHtml = $this->parseBlockContent( $blockContent );
if (!$parsedHtml) {
return '';
}
if ( ! $parsedHtml ) {
return '';
}
$imageUrl = $parsedHtml['imageUrl'];
$image = $parsedHtml['image'];
$caption = $parsedHtml['caption'];
$class = $parsedHtml['class'];
$parsedBlock = $this->addImageSizeWhenMissing($parsedBlock, $imageUrl, $settingsController);
$image = $this->addImageDimensions($image, $parsedBlock, $settingsController);
$imageUrl = $parsedHtml['imageUrl'];
$image = $parsedHtml['image'];
$caption = $parsedHtml['caption'];
$class = $parsedHtml['class'];
$parsedBlock = $this->addImageSizeWhenMissing( $parsedBlock, $imageUrl, $settingsController );
$image = $this->addImageDimensions( $image, $parsedBlock, $settingsController );
$imageWithWrapper = str_replace(
['{image_content}', '{caption_content}'],
[$image, $caption],
$this->getBlockWrapper($parsedBlock, $settingsController, $caption)
);
$imageWithWrapper = str_replace(
array( '{image_content}', '{caption_content}' ),
array( $image, $caption ),
$this->getBlockWrapper( $parsedBlock, $settingsController, $caption )
);
$imageWithWrapper = $this->applyRoundedStyle($imageWithWrapper, $parsedBlock);
$imageWithWrapper = $this->applyImageBorderStyle($imageWithWrapper, $parsedBlock, $class);
return $imageWithWrapper;
}
$imageWithWrapper = $this->applyRoundedStyle( $imageWithWrapper, $parsedBlock );
$imageWithWrapper = $this->applyImageBorderStyle( $imageWithWrapper, $parsedBlock, $class );
return $imageWithWrapper;
}
private function applyRoundedStyle(string $blockContent, array $parsedBlock): string {
// Because the isn't an attribute for definition of rounded style, we have to check the class name
if (isset($parsedBlock['attrs']['className']) && strpos($parsedBlock['attrs']['className'], 'is-style-rounded') !== false) {
// If the image should be in a circle, we need to set the border-radius to 9999px to make it the same as is in the editor
// This style is applied to both wrapper and the image
$blockContent = $this->removeStyleAttributeFromElement($blockContent, ['tag_name' => 'td', 'class_name' => 'email-image-cell'], 'border-radius');
$blockContent = $this->addStyleToElement($blockContent, ['tag_name' => 'td', 'class_name' => 'email-image-cell'], 'border-radius: 9999px;');
$blockContent = $this->removeStyleAttributeFromElement($blockContent, ['tag_name' => 'img'], 'border-radius');
$blockContent = $this->addStyleToElement($blockContent, ['tag_name' => 'img'], 'border-radius: 9999px;');
}
return $blockContent;
}
private function applyRoundedStyle( string $blockContent, array $parsedBlock ): string {
// Because the isn't an attribute for definition of rounded style, we have to check the class name
if ( isset( $parsedBlock['attrs']['className'] ) && strpos( $parsedBlock['attrs']['className'], 'is-style-rounded' ) !== false ) {
// If the image should be in a circle, we need to set the border-radius to 9999px to make it the same as is in the editor
// This style is applied to both wrapper and the image
$blockContent = $this->removeStyleAttributeFromElement(
$blockContent,
array(
'tag_name' => 'td',
'class_name' => 'email-image-cell',
),
'border-radius'
);
$blockContent = $this->addStyleToElement(
$blockContent,
array(
'tag_name' => 'td',
'class_name' => 'email-image-cell',
),
'border-radius: 9999px;'
);
$blockContent = $this->removeStyleAttributeFromElement( $blockContent, array( 'tag_name' => 'img' ), 'border-radius' );
$blockContent = $this->addStyleToElement( $blockContent, array( 'tag_name' => 'img' ), 'border-radius: 9999px;' );
}
return $blockContent;
}
/**
* When the width is not set, it's important to get it for the image to be displayed correctly
*/
private function addImageSizeWhenMissing(array $parsedBlock, string $imageUrl, Settings_Controller $settingsController): array {
if (isset($parsedBlock['attrs']['width'])) {
return $parsedBlock;
}
// Can't determine any width let's go with 100%
if (!isset($parsedBlock['email_attrs']['width'])) {
$parsedBlock['attrs']['width'] = '100%';
}
$maxWidth = $settingsController->parseNumberFromStringWithPixels($parsedBlock['email_attrs']['width']);
$imageSize = wp_getimagesize($imageUrl);
$imageSize = $imageSize ? $imageSize[0] : $maxWidth;
$width = min($imageSize, $maxWidth);
$parsedBlock['attrs']['width'] = "{$width}px";
return $parsedBlock;
/**
* When the width is not set, it's important to get it for the image to be displayed correctly
*/
private function addImageSizeWhenMissing( array $parsedBlock, string $imageUrl, Settings_Controller $settingsController ): array {
if ( isset( $parsedBlock['attrs']['width'] ) ) {
return $parsedBlock;
}
// Can't determine any width let's go with 100%
if ( ! isset( $parsedBlock['email_attrs']['width'] ) ) {
$parsedBlock['attrs']['width'] = '100%';
}
$maxWidth = $settingsController->parseNumberFromStringWithPixels( $parsedBlock['email_attrs']['width'] );
$imageSize = wp_getimagesize( $imageUrl );
$imageSize = $imageSize ? $imageSize[0] : $maxWidth;
$width = min( $imageSize, $maxWidth );
$parsedBlock['attrs']['width'] = "{$width}px";
return $parsedBlock;
}
}
private function applyImageBorderStyle( string $blockContent, array $parsedBlock, string $class ): string {
// Getting individual border properties
$borderStyles = wp_style_engine_get_styles( array( 'border' => $parsedBlock['attrs']['style']['border'] ?? array() ) );
$borderStyles = $borderStyles['declarations'] ?? array();
if ( ! empty( $borderStyles ) ) {
$borderStyles['border-style'] = 'solid';
$borderStyles['box-sizing'] = 'border-box';
}
$borderElementTag = array(
'tag_name' => 'td',
'class_name' => 'email-image-cell',
);
$contentWithBorderStyles = $this->addStyleToElement( $blockContent, $borderElementTag, \WP_Style_Engine::compile_css( $borderStyles, '' ) );
// Add Border related classes to proper element. This is required for inlined border-color styles when defined via class
$borderClasses = array_filter(
explode( ' ', $class ),
function ( $className ) {
return strpos( $className, 'border' ) !== false;
}
);
$html = new \WP_HTML_Tag_Processor( $contentWithBorderStyles );
if ( $html->next_tag( $borderElementTag ) ) {
$class = $html->get_attribute( 'class' ) ?? '';
$borderClasses[] = $class;
$html->set_attribute( 'class', implode( ' ', $borderClasses ) );
}
return $html->get_updated_html();
}
private function applyImageBorderStyle(string $blockContent, array $parsedBlock, string $class): string {
// Getting individual border properties
$borderStyles = wp_style_engine_get_styles(['border' => $parsedBlock['attrs']['style']['border'] ?? []]);
$borderStyles = $borderStyles['declarations'] ?? [];
if (!empty($borderStyles)) {
$borderStyles['border-style'] = 'solid';
$borderStyles['box-sizing'] = 'border-box';
}
$borderElementTag = ['tag_name' => 'td', 'class_name' => 'email-image-cell'];
$contentWithBorderStyles = $this->addStyleToElement($blockContent, $borderElementTag, \WP_Style_Engine::compile_css($borderStyles, ''));
// Add Border related classes to proper element. This is required for inlined border-color styles when defined via class
$borderClasses = array_filter(explode(' ', $class), function($className) {
return strpos($className, 'border') !== false;
});
$html = new \WP_HTML_Tag_Processor($contentWithBorderStyles);
if ($html->next_tag($borderElementTag)) {
$class = $html->get_attribute('class') ?? '';
$borderClasses[] = $class;
$html->set_attribute('class', implode(' ', $borderClasses));
}
return $html->get_updated_html();
}
/**
* Settings width and height attributes for images is important for MS Outlook.
*/
private function addImageDimensions( $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$html = new \WP_HTML_Tag_Processor( $blockContent );
if ( $html->next_tag( array( 'tag_name' => 'img' ) ) ) {
// Getting height from styles and if it's set, we set the height attribute
$styles = $html->get_attribute( 'style' ) ?? '';
$styles = $settingsController->parseStylesToArray( $styles );
$height = $styles['height'] ?? null;
if ( $height && $height !== 'auto' && is_numeric( $settingsController->parseNumberFromStringWithPixels( $height ) ) ) {
$height = $settingsController->parseNumberFromStringWithPixels( $height );
$html->set_attribute( 'height', esc_attr( $height ) );
}
/**
* Settings width and height attributes for images is important for MS Outlook.
*/
private function addImageDimensions($blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$html = new \WP_HTML_Tag_Processor($blockContent);
if ($html->next_tag(['tag_name' => 'img'])) {
// Getting height from styles and if it's set, we set the height attribute
$styles = $html->get_attribute('style') ?? '';
$styles = $settingsController->parseStylesToArray($styles);
$height = $styles['height'] ?? null;
if ($height && $height !== 'auto' && is_numeric($settingsController->parseNumberFromStringWithPixels($height))) {
$height = $settingsController->parseNumberFromStringWithPixels($height);
$html->set_attribute('height', esc_attr($height));
}
if ( isset( $parsedBlock['attrs']['width'] ) ) {
$width = $settingsController->parseNumberFromStringWithPixels( $parsedBlock['attrs']['width'] );
$html->set_attribute( 'width', esc_attr( $width ) );
}
$blockContent = $html->get_updated_html();
}
if (isset($parsedBlock['attrs']['width'])) {
$width = $settingsController->parseNumberFromStringWithPixels($parsedBlock['attrs']['width']);
$html->set_attribute('width', esc_attr($width));
}
$blockContent = $html->get_updated_html();
}
return $blockContent;
}
return $blockContent;
}
/**
* This method configure the font size of the caption because it's set to 0 for the parent element to avoid unexpected white spaces
* We try to use font-size passed down from the parent element $parsedBlock['email_attrs']['font-size'], but if it's not set, we use the default font-size from the email theme.
*/
private function getCaptionStyles( Settings_Controller $settingsController, array $parsedBlock ): string {
$themeData = $settingsController->getTheme()->get_data();
/**
* This method configure the font size of the caption because it's set to 0 for the parent element to avoid unexpected white spaces
* We try to use font-size passed down from the parent element $parsedBlock['email_attrs']['font-size'], but if it's not set, we use the default font-size from the email theme.
*/
private function getCaptionStyles(Settings_Controller $settingsController, array $parsedBlock): string {
$themeData = $settingsController->getTheme()->get_data();
$styles = array(
'text-align' => isset( $parsedBlock['attrs']['align'] ) ? 'center' : 'left',
);
$styles = [
'text-align' => isset($parsedBlock['attrs']['align']) ? 'center' : 'left',
];
$styles['font-size'] = $parsedBlock['email_attrs']['font-size'] ?? $themeData['styles']['typography']['fontSize'];
return \WP_Style_Engine::compile_css( $styles, '' );
}
$styles['font-size'] = $parsedBlock['email_attrs']['font-size'] ?? $themeData['styles']['typography']['fontSize'];
return \WP_Style_Engine::compile_css($styles, '');
}
/**
* Based on MJML <mj-image> but because MJML doesn't support captions, our solution is a bit different
*/
private function getBlockWrapper( array $parsedBlock, Settings_Controller $settingsController, ?string $caption ): string {
$styles = array(
'border-collapse' => 'collapse',
'border-spacing' => '0px',
'font-size' => '0px',
'vertical-align' => 'top',
'width' => '100%',
);
/**
* Based on MJML <mj-image> but because MJML doesn't support captions, our solution is a bit different
*/
private function getBlockWrapper(array $parsedBlock, Settings_Controller $settingsController, ?string $caption): string {
$styles = [
'border-collapse' => 'collapse',
'border-spacing' => '0px',
'font-size' => '0px',
'vertical-align' => 'top',
'width' => '100%',
];
$width = $parsedBlock['attrs']['width'] ?? '100%';
$wrapperWidth = ( $width && $width !== '100%' ) ? $width : 'auto';
$wrapperStyles = $styles;
$wrapperStyles['width'] = $wrapperWidth;
$wrapperStyles['border-collapse'] = 'separate'; // Needed because of border radius
$width = $parsedBlock['attrs']['width'] ?? '100%';
$wrapperWidth = ($width && $width !== '100%') ? $width : 'auto';
$wrapperStyles = $styles;
$wrapperStyles['width'] = $wrapperWidth;
$wrapperStyles['border-collapse'] = 'separate'; // Needed because of border radius
$captionHtml = '';
if ($caption) {
// When the image is not aligned, the wrapper is set to 100% width due to caption that can be longer than the image
$captionWidth = isset($parsedBlock['attrs']['align']) ? ($parsedBlock['attrs']['width'] ?? '100%') : '100%';
$captionWrapperStyles = $styles;
$captionWrapperStyles['width'] = $captionWidth;
$captionStyles = $this->getCaptionStyles($settingsController, $parsedBlock);
$captionHtml = '
$captionHtml = '';
if ( $caption ) {
// When the image is not aligned, the wrapper is set to 100% width due to caption that can be longer than the image
$captionWidth = isset( $parsedBlock['attrs']['align'] ) ? ( $parsedBlock['attrs']['width'] ?? '100%' ) : '100%';
$captionWrapperStyles = $styles;
$captionWrapperStyles['width'] = $captionWidth;
$captionStyles = $this->getCaptionStyles( $settingsController, $parsedBlock );
$captionHtml = '
<table
role="presentation"
class="email-table-with-width"
border="0"
cellpadding="0"
cellspacing="0"
style="' . esc_attr(\WP_Style_Engine::compile_css($captionWrapperStyles, '')) . '"
width="' . esc_attr($captionWidth) . '"
style="' . esc_attr( \WP_Style_Engine::compile_css( $captionWrapperStyles, '' ) ) . '"
width="' . esc_attr( $captionWidth ) . '"
>
<tr>
<td style="' . esc_attr($captionStyles) . '">{caption_content}</td>
<td style="' . esc_attr( $captionStyles ) . '">{caption_content}</td>
</tr>
</table>';
}
}
$styles['width'] = '100%';
$align = $parsedBlock['attrs']['align'] ?? 'left';
$styles['width'] = '100%';
$align = $parsedBlock['attrs']['align'] ?? 'left';
return '
return '
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="' . esc_attr(\WP_Style_Engine::compile_css($styles, '')) . '"
style="' . esc_attr( \WP_Style_Engine::compile_css( $styles, '' ) ) . '"
width="100%"
>
<tr>
<td align="' . esc_attr($align) . '">
<td align="' . esc_attr( $align ) . '">
<table
role="presentation"
class="email-table-with-width"
border="0"
cellpadding="0"
cellspacing="0"
style="' . esc_attr(\WP_Style_Engine::compile_css($wrapperStyles, '')) . '"
width="' . esc_attr($wrapperWidth) . '"
style="' . esc_attr( \WP_Style_Engine::compile_css( $wrapperStyles, '' ) ) . '"
width="' . esc_attr( $wrapperWidth ) . '"
>
<tr>
<td class="email-image-cell">{image_content}</td>
@ -199,83 +218,83 @@ class Image extends Abstract_Block_Renderer {
</tr>
</table>
';
}
}
/**
* @param array{tag_name: string, class_name?: string} $tag
* @param string $style
*/
private function addStyleToElement($blockContent, array $tag, string $style): string {
$html = new \WP_HTML_Tag_Processor($blockContent);
if ($html->next_tag($tag)) {
$elementStyle = $html->get_attribute('style') ?? '';
$elementStyle = !empty($elementStyle) ? (rtrim($elementStyle, ';') . ';') : ''; // Adding semicolon if it's missing
$elementStyle .= $style;
$html->set_attribute('style', esc_attr($elementStyle));
$blockContent = $html->get_updated_html();
}
/**
* @param array{tag_name: string, class_name?: string} $tag
* @param string $style
*/
private function addStyleToElement( $blockContent, array $tag, string $style ): string {
$html = new \WP_HTML_Tag_Processor( $blockContent );
if ( $html->next_tag( $tag ) ) {
$elementStyle = $html->get_attribute( 'style' ) ?? '';
$elementStyle = ! empty( $elementStyle ) ? ( rtrim( $elementStyle, ';' ) . ';' ) : ''; // Adding semicolon if it's missing
$elementStyle .= $style;
$html->set_attribute( 'style', esc_attr( $elementStyle ) );
$blockContent = $html->get_updated_html();
}
return $blockContent;
}
return $blockContent;
}
/**
* @param array{tag_name: string, class_name?: string} $tag
*/
private function removeStyleAttributeFromElement($blockContent, array $tag, string $styleName): string {
$html = new \WP_HTML_Tag_Processor($blockContent);
if ($html->next_tag($tag)) {
$elementStyle = $html->get_attribute('style') ?? '';
$elementStyle = preg_replace('/' . $styleName . ':(.?[0-9]+px)+;?/', '', $elementStyle);
$html->set_attribute('style', esc_attr($elementStyle));
$blockContent = $html->get_updated_html();
}
/**
* @param array{tag_name: string, class_name?: string} $tag
*/
private function removeStyleAttributeFromElement( $blockContent, array $tag, string $styleName ): string {
$html = new \WP_HTML_Tag_Processor( $blockContent );
if ( $html->next_tag( $tag ) ) {
$elementStyle = $html->get_attribute( 'style' ) ?? '';
$elementStyle = preg_replace( '/' . $styleName . ':(.?[0-9]+px)+;?/', '', $elementStyle );
$html->set_attribute( 'style', esc_attr( $elementStyle ) );
$blockContent = $html->get_updated_html();
}
return $blockContent;
}
return $blockContent;
}
/**
* @param string $blockContent
* @return array{imageUrl: string, image: string, caption: string}|null
*/
private function parseBlockContent(string $blockContent): ?array {
// If block's image is not set, we don't need to parse the content
if (empty($blockContent)) {
return null;
}
/**
* @param string $blockContent
* @return array{imageUrl: string, image: string, caption: string}|null
*/
private function parseBlockContent( string $blockContent ): ?array {
// If block's image is not set, we don't need to parse the content
if ( empty( $blockContent ) ) {
return null;
}
$domHelper = new Dom_Document_Helper($blockContent);
$domHelper = new Dom_Document_Helper( $blockContent );
$figureTag = $domHelper->findElement('figure');
if (!$figureTag) {
return null;
}
$figureTag = $domHelper->findElement( 'figure' );
if ( ! $figureTag ) {
return null;
}
$imgTag = $domHelper->findElement('img');
if (!$imgTag) {
return null;
}
$imgTag = $domHelper->findElement( 'img' );
if ( ! $imgTag ) {
return null;
}
$imageSrc = $domHelper->getAttributeValue($imgTag, 'src');
$imageClass = $domHelper->getAttributeValue($imgTag, 'class');
$imageHtml = $domHelper->getOuterHtml($imgTag);
$figcaption = $domHelper->findElement('figcaption');
$figcaptionHtml = $figcaption ? $domHelper->getOuterHtml($figcaption) : '';
$figcaptionHtml = str_replace(['<figcaption', '</figcaption>'], ['<span', '</span>'], $figcaptionHtml);
$imageSrc = $domHelper->getAttributeValue( $imgTag, 'src' );
$imageClass = $domHelper->getAttributeValue( $imgTag, 'class' );
$imageHtml = $domHelper->getOuterHtml( $imgTag );
$figcaption = $domHelper->findElement( 'figcaption' );
$figcaptionHtml = $figcaption ? $domHelper->getOuterHtml( $figcaption ) : '';
$figcaptionHtml = str_replace( array( '<figcaption', '</figcaption>' ), array( '<span', '</span>' ), $figcaptionHtml );
return [
'imageUrl' => $imageSrc ?: '',
'image' => $this->cleanupImageHtml($imageHtml),
'caption' => $figcaptionHtml ?: '',
'class' => $imageClass ?: '',
];
}
return array(
'imageUrl' => $imageSrc ?: '',
'image' => $this->cleanupImageHtml( $imageHtml ),
'caption' => $figcaptionHtml ?: '',
'class' => $imageClass ?: '',
);
}
private function cleanupImageHtml(string $contentHtml): string {
$html = new \WP_HTML_Tag_Processor($contentHtml);
if ($html->next_tag(['tag_name' => 'img'])) {
$html->remove_attribute('srcset');
$html->remove_attribute('class');
}
return $html->get_updated_html();
}
private function cleanupImageHtml( string $contentHtml ): string {
$html = new \WP_HTML_Tag_Processor( $contentHtml );
if ( $html->next_tag( array( 'tag_name' => 'img' ) ) ) {
$html->remove_attribute( 'srcset' );
$html->remove_attribute( 'class' );
}
return $html->get_updated_html();
}
}

View File

@ -6,37 +6,40 @@ use MailPoet\EmailEditor\Engine\Settings_Controller;
// We have to avoid using keyword `List`
class List_Block extends Abstract_Block_Renderer {
protected function renderContent(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
$html = new \WP_HTML_Tag_Processor($blockContent);
$tagName = ($parsedBlock['attrs']['ordered'] ?? false) ? 'ol' : 'ul';
if ($html->next_tag(['tag_name' => $tagName])) {
$styles = $html->get_attribute('style') ?? '';
$styles = $settingsController->parseStylesToArray($styles);
protected function renderContent( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
$html = new \WP_HTML_Tag_Processor( $blockContent );
$tagName = ( $parsedBlock['attrs']['ordered'] ?? false ) ? 'ol' : 'ul';
if ( $html->next_tag( array( 'tag_name' => $tagName ) ) ) {
$styles = $html->get_attribute( 'style' ) ?? '';
$styles = $settingsController->parseStylesToArray( $styles );
// Font size
if (isset($parsedBlock['email_attrs']['font-size'])) {
$styles['font-size'] = $parsedBlock['email_attrs']['font-size'];
} else {
// Use font-size from email theme when those properties are not set
$themeData = $settingsController->getTheme()->get_data();
$styles['font-size'] = $themeData['styles']['typography']['fontSize'];
}
// Font size
if ( isset( $parsedBlock['email_attrs']['font-size'] ) ) {
$styles['font-size'] = $parsedBlock['email_attrs']['font-size'];
} else {
// Use font-size from email theme when those properties are not set
$themeData = $settingsController->getTheme()->get_data();
$styles['font-size'] = $themeData['styles']['typography']['fontSize'];
}
$html->set_attribute('style', esc_attr(\WP_Style_Engine::compile_css($styles, '')));
$blockContent = $html->get_updated_html();
}
$html->set_attribute( 'style', esc_attr( \WP_Style_Engine::compile_css( $styles, '' ) ) );
$blockContent = $html->get_updated_html();
}
$wrapperStyle = \WP_Style_Engine::compile_css([
'margin-top' => $parsedBlock['email_attrs']['margin-top'] ?? '0px',
], '');
$wrapperStyle = \WP_Style_Engine::compile_css(
array(
'margin-top' => $parsedBlock['email_attrs']['margin-top'] ?? '0px',
),
''
);
// \WP_HTML_Tag_Processor escapes the content, so we have to replace it back
$blockContent = str_replace('&#039;', "'", $blockContent);
// \WP_HTML_Tag_Processor escapes the content, so we have to replace it back
$blockContent = str_replace( '&#039;', "'", $blockContent );
return sprintf(
'<div style="%1$s">%2$s</div>',
esc_attr($wrapperStyle),
$blockContent
);
}
return sprintf(
'<div style="%1$s">%2$s</div>',
esc_attr( $wrapperStyle ),
$blockContent
);
}
}

View File

@ -5,14 +5,14 @@ namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class List_Item extends Abstract_Block_Renderer {
/**
* Override this method to disable spacing (block gap) for list items.
*/
protected function addSpacer($content, $emailAttrs): string {
return $content;
}
/**
* Override this method to disable spacing (block gap) for list items.
*/
protected function addSpacer( $content, $emailAttrs ): string {
return $content;
}
protected function renderContent($blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
return $blockContent;
}
protected function renderContent( $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
return $blockContent;
}
}

View File

@ -8,53 +8,58 @@ use MailPoet\EmailEditor\Engine\Settings_Controller;
* This renderer covers both core/paragraph and core/heading blocks
*/
class Text extends Abstract_Block_Renderer {
protected function renderContent(string $blockContent, array $parsedBlock, Settings_Controller $settingsController): string {
// Do not render empty blocks.
if (empty(trim(strip_tags($blockContent)))) {
return '';
}
protected function renderContent( string $blockContent, array $parsedBlock, Settings_Controller $settingsController ): string {
// Do not render empty blocks.
if ( empty( trim( strip_tags( $blockContent ) ) ) ) {
return '';
}
$blockContent = $this->adjustStyleAttribute($blockContent);
$blockAttributes = wp_parse_args($parsedBlock['attrs'] ?? [], [
'textAlign' => 'left',
'style' => [],
]);
$html = new \WP_HTML_Tag_Processor($blockContent);
$classes = 'email-text-block';
if ($html->next_tag()) {
$blockClasses = $html->get_attribute('class') ?? '';
$classes .= ' ' . $blockClasses;
// remove has-background to prevent double padding applied for wrapper and inner element
$blockClasses = str_replace('has-background', '', $blockClasses);
// remove border related classes because we handle border on wrapping table cell
$blockClasses = preg_replace('/[a-z-]+-border-[a-z-]+/', '', $blockClasses);
$html->set_attribute('class', trim($blockClasses));
$blockContent = $html->get_updated_html();
}
$blockContent = $this->adjustStyleAttribute( $blockContent );
$blockAttributes = wp_parse_args(
$parsedBlock['attrs'] ?? array(),
array(
'textAlign' => 'left',
'style' => array(),
)
);
$html = new \WP_HTML_Tag_Processor( $blockContent );
$classes = 'email-text-block';
if ( $html->next_tag() ) {
$blockClasses = $html->get_attribute( 'class' ) ?? '';
$classes .= ' ' . $blockClasses;
// remove has-background to prevent double padding applied for wrapper and inner element
$blockClasses = str_replace( 'has-background', '', $blockClasses );
// remove border related classes because we handle border on wrapping table cell
$blockClasses = preg_replace( '/[a-z-]+-border-[a-z-]+/', '', $blockClasses );
$html->set_attribute( 'class', trim( $blockClasses ) );
$blockContent = $html->get_updated_html();
}
$blockStyles = $this->getStylesFromBlock([
'color' => $blockAttributes['style']['color'] ?? [],
'spacing' => $blockAttributes['style']['spacing'] ?? [],
'typography' => $blockAttributes['style']['typography'] ?? [],
'border' => $blockAttributes['style']['border'] ?? [],
]);
$blockStyles = $this->getStylesFromBlock(
array(
'color' => $blockAttributes['style']['color'] ?? array(),
'spacing' => $blockAttributes['style']['spacing'] ?? array(),
'typography' => $blockAttributes['style']['typography'] ?? array(),
'border' => $blockAttributes['style']['border'] ?? array(),
)
);
$styles = [
'min-width' => '100%', // prevent Gmail App from shrinking the table on mobile devices
];
$styles = array(
'min-width' => '100%', // prevent Gmail App from shrinking the table on mobile devices
);
$styles['text-align'] = 'left';
if (isset($parsedBlock['attrs']['textAlign'])) {
$styles['text-align'] = $parsedBlock['attrs']['textAlign'];
} elseif (in_array($parsedBlock['attrs']['align'] ?? null, ['left', 'center', 'right'])) {
$styles['text-align'] = $parsedBlock['attrs']['align'];
}
$styles['text-align'] = 'left';
if ( isset( $parsedBlock['attrs']['textAlign'] ) ) {
$styles['text-align'] = $parsedBlock['attrs']['textAlign'];
} elseif ( in_array( $parsedBlock['attrs']['align'] ?? null, array( 'left', 'center', 'right' ) ) ) {
$styles['text-align'] = $parsedBlock['attrs']['align'];
}
$compiledStyles = $this->compileCss($blockStyles['declarations'], $styles);
$tableStyles = 'border-collapse: separate;'; // Needed because of border radius
$compiledStyles = $this->compileCss( $blockStyles['declarations'], $styles );
$tableStyles = 'border-collapse: separate;'; // Needed because of border radius
return sprintf(
'<table
return sprintf(
'<table
role="presentation"
border="0"
cellpadding="0"
@ -66,39 +71,39 @@ class Text extends Abstract_Block_Renderer {
<td class="%2$s" style="%3$s" align="%4$s">%5$s</td>
</tr>
</table>',
esc_attr($tableStyles),
esc_attr($classes),
esc_attr($compiledStyles),
esc_attr($styles['text-align'] ?? 'left'),
$blockContent
);
}
esc_attr( $tableStyles ),
esc_attr( $classes ),
esc_attr( $compiledStyles ),
esc_attr( $styles['text-align'] ?? 'left' ),
$blockContent
);
}
/**
* 1) We need to remove padding because we render padding on wrapping table cell
* 2) We also need to replace font-size to avoid clamp() because clamp() is not supported in many email clients.
* The font size values is automatically converted to clamp() when WP site theme is configured to use fluid layouts.
* Currently (WP 6.5), there is no way to disable this behavior.
*/
private function adjustStyleAttribute(string $blockContent): string {
$html = new \WP_HTML_Tag_Processor($blockContent);
/**
* 1) We need to remove padding because we render padding on wrapping table cell
* 2) We also need to replace font-size to avoid clamp() because clamp() is not supported in many email clients.
* The font size values is automatically converted to clamp() when WP site theme is configured to use fluid layouts.
* Currently (WP 6.5), there is no way to disable this behavior.
*/
private function adjustStyleAttribute( string $blockContent ): string {
$html = new \WP_HTML_Tag_Processor( $blockContent );
if ($html->next_tag()) {
$elementStyle = $html->get_attribute('style') ?? '';
// Padding may contain value like 10px or variable like var(--spacing-10)
$elementStyle = preg_replace('/padding[^:]*:.?[0-9a-z-()]+;?/', '', $elementStyle);
if ( $html->next_tag() ) {
$elementStyle = $html->get_attribute( 'style' ) ?? '';
// Padding may contain value like 10px or variable like var(--spacing-10)
$elementStyle = preg_replace( '/padding[^:]*:.?[0-9a-z-()]+;?/', '', $elementStyle );
// Remove border styles. We apply border styles on the wrapping table cell
$elementStyle = preg_replace('/border[^:]*:.?[0-9a-z-()#]+;?/', '', $elementStyle);
// Remove border styles. We apply border styles on the wrapping table cell
$elementStyle = preg_replace( '/border[^:]*:.?[0-9a-z-()#]+;?/', '', $elementStyle );
// We define the font-size on the wrapper element, but we need to keep font-size definition here
// to prevent CSS Inliner from adding a default value and overriding the value set by user, which is on the wrapper element.
// The value provided by WP uses clamp() function which is not supported in many email clients
$elementStyle = preg_replace('/font-size:[^;]+;?/', 'font-size: inherit;', $elementStyle);
$html->set_attribute('style', esc_attr($elementStyle));
$blockContent = $html->get_updated_html();
}
// We define the font-size on the wrapper element, but we need to keep font-size definition here
// to prevent CSS Inliner from adding a default value and overriding the value set by user, which is on the wrapper element.
// The value provided by WP uses clamp() function which is not supported in many email clients
$elementStyle = preg_replace( '/font-size:[^;]+;?/', 'font-size: inherit;', $elementStyle );
$html->set_attribute( 'style', esc_attr( $elementStyle ) );
$blockContent = $html->get_updated_html();
}
return $blockContent;
}
return $blockContent;
}
}

View File

@ -6,49 +6,49 @@ use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Blocks_Registry;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Layout\Flex_Layout_Renderer;
class Initializer {
public function initialize(): void {
add_action('mailpoet_blocks_renderer_initialized', [$this, 'registerCoreBlocksRenderers'], 10, 1);
add_filter('mailpoet_email_editor_theme_json', [$this, 'adjustThemeJson'], 10, 1);
add_filter('safe_style_css', [$this, 'allowStyles']);
}
public function initialize(): void {
add_action( 'mailpoet_blocks_renderer_initialized', array( $this, 'registerCoreBlocksRenderers' ), 10, 1 );
add_filter( 'mailpoet_email_editor_theme_json', array( $this, 'adjustThemeJson' ), 10, 1 );
add_filter( 'safe_style_css', array( $this, 'allowStyles' ) );
}
/**
* Register core blocks email renderers when the blocks renderer is initialized.
*/
public function registerCoreBlocksRenderers(Blocks_Registry $blocksRegistry): void {
$blocksRegistry->addBlockRenderer('core/paragraph', new Renderer\Blocks\Text());
$blocksRegistry->addBlockRenderer('core/heading', new Renderer\Blocks\Text());
$blocksRegistry->addBlockRenderer('core/column', new Renderer\Blocks\Column());
$blocksRegistry->addBlockRenderer('core/columns', new Renderer\Blocks\Columns());
$blocksRegistry->addBlockRenderer('core/list', new Renderer\Blocks\List_Block());
$blocksRegistry->addBlockRenderer('core/list-item', new Renderer\Blocks\List_Item());
$blocksRegistry->addBlockRenderer('core/image', new Renderer\Blocks\Image());
$blocksRegistry->addBlockRenderer('core/buttons', new Renderer\Blocks\Buttons(new Flex_Layout_Renderer()));
$blocksRegistry->addBlockRenderer('core/button', new Renderer\Blocks\Button());
$blocksRegistry->addBlockRenderer('core/group', new Renderer\Blocks\Group());
// Render used for all other blocks
$blocksRegistry->addFallbackRenderer(new Renderer\Blocks\Fallback());
}
/**
* Register core blocks email renderers when the blocks renderer is initialized.
*/
public function registerCoreBlocksRenderers( Blocks_Registry $blocksRegistry ): void {
$blocksRegistry->addBlockRenderer( 'core/paragraph', new Renderer\Blocks\Text() );
$blocksRegistry->addBlockRenderer( 'core/heading', new Renderer\Blocks\Text() );
$blocksRegistry->addBlockRenderer( 'core/column', new Renderer\Blocks\Column() );
$blocksRegistry->addBlockRenderer( 'core/columns', new Renderer\Blocks\Columns() );
$blocksRegistry->addBlockRenderer( 'core/list', new Renderer\Blocks\List_Block() );
$blocksRegistry->addBlockRenderer( 'core/list-item', new Renderer\Blocks\List_Item() );
$blocksRegistry->addBlockRenderer( 'core/image', new Renderer\Blocks\Image() );
$blocksRegistry->addBlockRenderer( 'core/buttons', new Renderer\Blocks\Buttons( new Flex_Layout_Renderer() ) );
$blocksRegistry->addBlockRenderer( 'core/button', new Renderer\Blocks\Button() );
$blocksRegistry->addBlockRenderer( 'core/group', new Renderer\Blocks\Group() );
// Render used for all other blocks
$blocksRegistry->addFallbackRenderer( new Renderer\Blocks\Fallback() );
}
/**
* Adjusts the editor's theme to add blocks specific settings for core blocks.
*/
public function adjustThemeJson(\WP_Theme_JSON $editorThemeJson): \WP_Theme_JSON {
$themeJson = (string)file_get_contents(dirname(__FILE__) . '/theme.json');
$themeJson = json_decode($themeJson, true);
/** @var array $themeJson */
$editorThemeJson->merge(new \WP_Theme_JSON($themeJson, 'default'));
return $editorThemeJson;
}
/**
* Adjusts the editor's theme to add blocks specific settings for core blocks.
*/
public function adjustThemeJson( \WP_Theme_JSON $editorThemeJson ): \WP_Theme_JSON {
$themeJson = (string) file_get_contents( __DIR__ . '/theme.json' );
$themeJson = json_decode( $themeJson, true );
/** @var array $themeJson */
$editorThemeJson->merge( new \WP_Theme_JSON( $themeJson, 'default' ) );
return $editorThemeJson;
}
/**
* Allow styles for the email editor.
*/
public function allowStyles(array $allowedStyles): array {
$allowedStyles[] = 'display';
$allowedStyles[] = 'mso-padding-alt';
$allowedStyles[] = 'mso-font-width';
$allowedStyles[] = 'mso-text-raise';
return $allowedStyles;
}
/**
* Allow styles for the email editor.
*/
public function allowStyles( array $allowedStyles ): array {
$allowedStyles[] = 'display';
$allowedStyles[] = 'mso-padding-alt';
$allowedStyles[] = 'mso-font-width';
$allowedStyles[] = 'mso-text-raise';
return $allowedStyles;
}
}

View File

@ -6,55 +6,57 @@ namespace MailPoet\EmailEditor\Integrations\Utils;
* This class should guarantee that our work with the DOMDocument is unified and safe.
*/
class Dom_Document_Helper {
private \DOMDocument $dom;
private \DOMDocument $dom;
public function __construct(
string $htmlContent
) {
$this->loadHtml($htmlContent);
}
public function __construct(
string $htmlContent
) {
$this->loadHtml( $htmlContent );
}
private function loadHtml(string $htmlContent): void {
libxml_use_internal_errors(true);
$this->dom = new \DOMDocument();
if (!empty($htmlContent)) {
// prefixing the content with the XML declaration to force the input encoding to UTF-8
$this->dom->loadHTML('<?xml encoding="UTF-8">' . $htmlContent, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
}
libxml_clear_errors();
}
private function loadHtml( string $htmlContent ): void {
libxml_use_internal_errors( true );
$this->dom = new \DOMDocument();
if ( ! empty( $htmlContent ) ) {
// prefixing the content with the XML declaration to force the input encoding to UTF-8
$this->dom->loadHTML( '<?xml encoding="UTF-8">' . $htmlContent, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
}
libxml_clear_errors();
}
public function findElement(string $tagName): ?\DOMElement {
$elements = $this->dom->getElementsByTagName($tagName);
return $elements->item(0) ?: null;
}
public function findElement( string $tagName ): ?\DOMElement {
$elements = $this->dom->getElementsByTagName( $tagName );
return $elements->item( 0 ) ?: null;
}
public function getAttributeValue(\DOMElement $element, string $attribute): string {
return $element->hasAttribute($attribute) ? $element->getAttribute($attribute) : '';
}
public function getAttributeValue( \DOMElement $element, string $attribute ): string {
return $element->hasAttribute( $attribute ) ? $element->getAttribute( $attribute ) : '';
}
/**
* Searches for the first appearance of the given tag name and returns the value of specified attribute.
*/
public function getAttributeValueByTagName(string $tagName, string $attribute): ?string {
$element = $this->findElement($tagName);
if (!$element) {
return null;
}
return $this->getAttributeValue($element, $attribute);
}
/**
* Searches for the first appearance of the given tag name and returns the value of specified attribute.
*/
public function getAttributeValueByTagName( string $tagName, string $attribute ): ?string {
$element = $this->findElement( $tagName );
if ( ! $element ) {
return null;
}
return $this->getAttributeValue( $element, $attribute );
}
public function getOuterHtml(\DOMElement $element): string {
return (string)$this->dom->saveHTML($element);
}
public function getOuterHtml( \DOMElement $element ): string {
return (string) $this->dom->saveHTML( $element );
}
public function getElementInnerHTML(\DOMElement $element): string {
$innerHTML = '';
$children = $element->childNodes;
foreach ($children as $child) {
if (!$child instanceof \DOMNode) continue;
$innerHTML .= $this->dom->saveHTML($child);
}
return $innerHTML;
}
public function getElementInnerHTML( \DOMElement $element ): string {
$innerHTML = '';
$children = $element->childNodes;
foreach ( $children as $child ) {
if ( ! $child instanceof \DOMNode ) {
continue;
}
$innerHTML .= $this->dom->saveHTML( $child );
}
return $innerHTML;
}
}

View File

@ -3,18 +3,18 @@
namespace MailPoet\EmailEditor\Utils;
class Cdn_Asset_Url {
const CDN_URL = 'https://ps.w.org/mailpoet/';
/** @var string */
private $baseUrl;
const CDN_URL = 'https://ps.w.org/mailpoet/';
/** @var string */
private $baseUrl;
public function __construct(
string $baseUrl
) {
$this->baseUrl = $baseUrl;
}
public function __construct(
string $baseUrl
) {
$this->baseUrl = $baseUrl;
}
public function generateCdnUrl($path) {
$useCdn = defined('MAILPOET_USE_CDN') ? MAILPOET_USE_CDN : true;
return ($useCdn ? self::CDN_URL : $this->baseUrl . '/plugin_repository/') . "assets/$path";
}
public function generateCdnUrl( $path ) {
$useCdn = defined( 'MAILPOET_USE_CDN' ) ? MAILPOET_USE_CDN : true;
return ( $useCdn ? self::CDN_URL : $this->baseUrl . '/plugin_repository/' ) . "assets/$path";
}
}

View File

@ -6,32 +6,35 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#oneof-and-anyof
class Any_Of_Schema extends Schema {
protected $schema = [
'anyOf' => [],
];
protected $schema = array(
'anyOf' => array(),
);
/** @param Schema[] $schemas */
public function __construct(
array $schemas
) {
foreach ($schemas as $schema) {
$this->schema['anyOf'][] = $schema->toArray();
}
}
/** @param Schema[] $schemas */
public function __construct(
array $schemas
) {
foreach ( $schemas as $schema ) {
$this->schema['anyOf'][] = $schema->toArray();
}
}
public function nullable(): self {
$null = ['type' => 'null'];
$anyOf = $this->schema['anyOf'];
$value = in_array($null, $anyOf, true) ? $anyOf : array_merge($anyOf, [$null]);
return $this->updateSchemaProperty('anyOf', $value);
}
public function nullable(): self {
$null = array( 'type' => 'null' );
$anyOf = $this->schema['anyOf'];
$value = in_array( $null, $anyOf, true ) ? $anyOf : array_merge( $anyOf, array( $null ) );
return $this->updateSchemaProperty( 'anyOf', $value );
}
public function nonNullable(): self {
$null = ['type' => 'null'];
$anyOf = $this->schema['anyOf'];
$value = array_filter($anyOf, function ($item) use ($null) {
return $item !== $null;
});
return $this->updateSchemaProperty('anyOf', $value);
}
public function nonNullable(): self {
$null = array( 'type' => 'null' );
$anyOf = $this->schema['anyOf'];
$value = array_filter(
$anyOf,
function ( $item ) use ( $null ) {
return $item !== $null;
}
);
return $this->updateSchemaProperty( 'anyOf', $value );
}
}

View File

@ -6,23 +6,23 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#arrays
class Array_Schema extends Schema {
protected $schema = [
'type' => 'array',
];
protected $schema = array(
'type' => 'array',
);
public function items(Schema $schema): self {
return $this->updateSchemaProperty('items', $schema->toArray());
}
public function items( Schema $schema ): self {
return $this->updateSchemaProperty( 'items', $schema->toArray() );
}
public function minItems(int $value): self {
return $this->updateSchemaProperty('minItems', $value);
}
public function minItems( int $value ): self {
return $this->updateSchemaProperty( 'minItems', $value );
}
public function maxItems(int $value): self {
return $this->updateSchemaProperty('maxItems', $value);
}
public function maxItems( int $value ): self {
return $this->updateSchemaProperty( 'maxItems', $value );
}
public function uniqueItems(): self {
return $this->updateSchemaProperty('uniqueItems', true);
}
public function uniqueItems(): self {
return $this->updateSchemaProperty( 'uniqueItems', true );
}
}

View File

@ -6,7 +6,7 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types
class Boolean_Schema extends Schema {
protected $schema = [
'type' => 'boolean',
];
protected $schema = array(
'type' => 'boolean',
);
}

View File

@ -6,31 +6,31 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#numbers
class Integer_Schema extends Schema {
protected $schema = [
'type' => 'integer',
];
protected $schema = array(
'type' => 'integer',
);
public function minimum(int $value): self {
return $this->updateSchemaProperty('minimum', $value)
->unsetSchemaProperty('exclusiveMinimum');
}
public function minimum( int $value ): self {
return $this->updateSchemaProperty( 'minimum', $value )
->unsetSchemaProperty( 'exclusiveMinimum' );
}
public function exclusiveMinimum(int $value): self {
return $this->updateSchemaProperty('minimum', $value)
->updateSchemaProperty('exclusiveMinimum', true);
}
public function exclusiveMinimum( int $value ): self {
return $this->updateSchemaProperty( 'minimum', $value )
->updateSchemaProperty( 'exclusiveMinimum', true );
}
public function maximum(int $value): self {
return $this->updateSchemaProperty('maximum', $value)
->unsetSchemaProperty('exclusiveMaximum');
}
public function maximum( int $value ): self {
return $this->updateSchemaProperty( 'maximum', $value )
->unsetSchemaProperty( 'exclusiveMaximum' );
}
public function exclusiveMaximum(int $value): self {
return $this->updateSchemaProperty('maximum', $value)
->updateSchemaProperty('exclusiveMaximum', true);
}
public function exclusiveMaximum( int $value ): self {
return $this->updateSchemaProperty( 'maximum', $value )
->updateSchemaProperty( 'exclusiveMaximum', true );
}
public function multipleOf(int $value): self {
return $this->updateSchemaProperty('multipleOf', $value);
}
public function multipleOf( int $value ): self {
return $this->updateSchemaProperty( 'multipleOf', $value );
}
}

View File

@ -6,7 +6,7 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#primitive-types
class Null_Schema extends Schema {
protected $schema = [
'type' => 'null',
];
protected $schema = array(
'type' => 'null',
);
}

View File

@ -6,31 +6,31 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#numbers
class Number_Schema extends Schema {
protected $schema = [
'type' => 'number',
];
protected $schema = array(
'type' => 'number',
);
public function minimum(float $value): self {
return $this->updateSchemaProperty('minimum', $value)
->unsetSchemaProperty('exclusiveMinimum');
}
public function minimum( float $value ): self {
return $this->updateSchemaProperty( 'minimum', $value )
->unsetSchemaProperty( 'exclusiveMinimum' );
}
public function exclusiveMinimum(float $value): self {
return $this->updateSchemaProperty('minimum', $value)
->updateSchemaProperty('exclusiveMinimum', true);
}
public function exclusiveMinimum( float $value ): self {
return $this->updateSchemaProperty( 'minimum', $value )
->updateSchemaProperty( 'exclusiveMinimum', true );
}
public function maximum(float $value): self {
return $this->updateSchemaProperty('maximum', $value)
->unsetSchemaProperty('exclusiveMaximum');
}
public function maximum( float $value ): self {
return $this->updateSchemaProperty( 'maximum', $value )
->unsetSchemaProperty( 'exclusiveMaximum' );
}
public function exclusiveMaximum(float $value): self {
return $this->updateSchemaProperty('maximum', $value)
->updateSchemaProperty('exclusiveMaximum', true);
}
public function exclusiveMaximum( float $value ): self {
return $this->updateSchemaProperty( 'maximum', $value )
->updateSchemaProperty( 'exclusiveMaximum', true );
}
public function multipleOf(float $value): self {
return $this->updateSchemaProperty('multipleOf', $value);
}
public function multipleOf( float $value ): self {
return $this->updateSchemaProperty( 'multipleOf', $value );
}
}

View File

@ -6,48 +6,51 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#objects
class Object_Schema extends Schema {
protected $schema = [
'type' => 'object',
];
protected $schema = array(
'type' => 'object',
);
/** @param array<string, Schema> $properties */
public function properties(array $properties): self {
return $this->updateSchemaProperty('properties', array_map(
function (Schema $property) {
return $property->toArray();
},
$properties
));
}
/** @param array<string, Schema> $properties */
public function properties( array $properties ): self {
return $this->updateSchemaProperty(
'properties',
array_map(
function ( Schema $property ) {
return $property->toArray();
},
$properties
)
);
}
public function additionalProperties(Schema $schema): self {
return $this->updateSchemaProperty('additionalProperties', $schema->toArray());
}
public function additionalProperties( Schema $schema ): self {
return $this->updateSchemaProperty( 'additionalProperties', $schema->toArray() );
}
public function disableAdditionalProperties(): self {
return $this->updateSchemaProperty('additionalProperties', false);
}
public function disableAdditionalProperties(): self {
return $this->updateSchemaProperty( 'additionalProperties', false );
}
/**
* Keys of $properties are regular expressions without leading/trailing delimiters.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#patternproperties
*
* @param array<string, Schema> $properties
*/
public function patternProperties(array $properties): self {
$patternProperties = [];
foreach ($properties as $key => $value) {
$this->validatePattern($key);
$patternProperties[$key] = $value->toArray();
}
return $this->updateSchemaProperty('patternProperties', $patternProperties);
}
/**
* Keys of $properties are regular expressions without leading/trailing delimiters.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#patternproperties
*
* @param array<string, Schema> $properties
*/
public function patternProperties( array $properties ): self {
$patternProperties = array();
foreach ( $properties as $key => $value ) {
$this->validatePattern( $key );
$patternProperties[ $key ] = $value->toArray();
}
return $this->updateSchemaProperty( 'patternProperties', $patternProperties );
}
public function minProperties(int $value): self {
return $this->updateSchemaProperty('minProperties', $value);
}
public function minProperties( int $value ): self {
return $this->updateSchemaProperty( 'minProperties', $value );
}
public function maxProperties(int $value): self {
return $this->updateSchemaProperty('maxProperties', $value);
}
public function maxProperties( int $value ): self {
return $this->updateSchemaProperty( 'maxProperties', $value );
}
}

View File

@ -6,32 +6,35 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#oneof-and-anyof
class One_Of_Schema extends Schema {
protected $schema = [
'oneOf' => [],
];
protected $schema = array(
'oneOf' => array(),
);
/** @param Schema[] $schemas */
public function __construct(
array $schemas
) {
foreach ($schemas as $schema) {
$this->schema['oneOf'][] = $schema->toArray();
}
}
/** @param Schema[] $schemas */
public function __construct(
array $schemas
) {
foreach ( $schemas as $schema ) {
$this->schema['oneOf'][] = $schema->toArray();
}
}
public function nullable(): self {
$null = ['type' => 'null'];
$oneOf = $this->schema['oneOf'];
$value = in_array($null, $oneOf, true) ? $oneOf : array_merge($oneOf, [$null]);
return $this->updateSchemaProperty('oneOf', $value);
}
public function nullable(): self {
$null = array( 'type' => 'null' );
$oneOf = $this->schema['oneOf'];
$value = in_array( $null, $oneOf, true ) ? $oneOf : array_merge( $oneOf, array( $null ) );
return $this->updateSchemaProperty( 'oneOf', $value );
}
public function nonNullable(): self {
$null = ['type' => 'null'];
$oneOf = $this->schema['oneOf'];
$value = array_filter($oneOf, function ($item) use ($null) {
return $item !== $null;
});
return $this->updateSchemaProperty('oneOf', $value);
}
public function nonNullable(): self {
$null = array( 'type' => 'null' );
$oneOf = $this->schema['oneOf'];
$value = array_filter(
$oneOf,
function ( $item ) use ( $null ) {
return $item !== $null;
}
);
return $this->updateSchemaProperty( 'oneOf', $value );
}
}

View File

@ -6,48 +6,48 @@ use MailPoet\EmailEditor\Validator\Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#strings
class String_Schema extends Schema {
protected $schema = [
'type' => 'string',
];
protected $schema = array(
'type' => 'string',
);
public function minLength(int $value): self {
return $this->updateSchemaProperty('minLength', $value);
}
public function minLength( int $value ): self {
return $this->updateSchemaProperty( 'minLength', $value );
}
public function maxLength(int $value): self {
return $this->updateSchemaProperty('maxLength', $value);
}
public function maxLength( int $value ): self {
return $this->updateSchemaProperty( 'maxLength', $value );
}
/**
* Parameter $pattern is a regular expression without leading/trailing delimiters.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#pattern
*/
public function pattern(string $pattern): self {
$this->validatePattern($pattern);
return $this->updateSchemaProperty('pattern', $pattern);
}
/**
* Parameter $pattern is a regular expression without leading/trailing delimiters.
* See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#pattern
*/
public function pattern( string $pattern ): self {
$this->validatePattern( $pattern );
return $this->updateSchemaProperty( 'pattern', $pattern );
}
public function formatDateTime(): self {
return $this->updateSchemaProperty('format', 'date-time');
}
public function formatDateTime(): self {
return $this->updateSchemaProperty( 'format', 'date-time' );
}
public function formatEmail(): self {
return $this->updateSchemaProperty('format', 'email');
}
public function formatEmail(): self {
return $this->updateSchemaProperty( 'format', 'email' );
}
public function formatHexColor(): self {
return $this->updateSchemaProperty('format', 'hex-color');
}
public function formatHexColor(): self {
return $this->updateSchemaProperty( 'format', 'hex-color' );
}
public function formatIp(): self {
return $this->updateSchemaProperty('format', 'ip');
}
public function formatIp(): self {
return $this->updateSchemaProperty( 'format', 'ip' );
}
public function formatUri(): self {
return $this->updateSchemaProperty('format', 'uri');
}
public function formatUri(): self {
return $this->updateSchemaProperty( 'format', 'uri' );
}
public function formatUuid(): self {
return $this->updateSchemaProperty('format', 'uuid');
}
public function formatUuid(): self {
return $this->updateSchemaProperty( 'format', 'uuid' );
}
}

View File

@ -14,44 +14,44 @@ use MailPoet\EmailEditor\Validator\Schema\String_Schema;
// See: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/
class Builder {
public static function string(): String_Schema {
return new String_Schema();
}
public static function string(): String_Schema {
return new String_Schema();
}
public static function number(): Number_Schema {
return new Number_Schema();
}
public static function number(): Number_Schema {
return new Number_Schema();
}
public static function integer(): Integer_Schema {
return new Integer_Schema();
}
public static function integer(): Integer_Schema {
return new Integer_Schema();
}
public static function boolean(): Boolean_Schema {
return new Boolean_Schema();
}
public static function boolean(): Boolean_Schema {
return new Boolean_Schema();
}
public static function null(): Null_Schema {
return new Null_Schema();
}
public static function null(): Null_Schema {
return new Null_Schema();
}
public static function array(Schema $items = null): Array_Schema {
$array = new Array_Schema();
return $items ? $array->items($items) : $array;
}
public static function array( Schema $items = null ): Array_Schema {
$array = new Array_Schema();
return $items ? $array->items( $items ) : $array;
}
/** @param array<string, Schema>|null $properties */
public static function object(array $properties = null): Object_Schema {
$object = new Object_Schema();
return $properties === null ? $object : $object->properties($properties);
}
/** @param array<string, Schema>|null $properties */
public static function object( array $properties = null ): Object_Schema {
$object = new Object_Schema();
return $properties === null ? $object : $object->properties( $properties );
}
/** @param Schema[] $schemas */
public static function oneOf(array $schemas): One_Of_Schema {
return new One_Of_Schema($schemas);
}
/** @param Schema[] $schemas */
public static function oneOf( array $schemas ): One_Of_Schema {
return new One_Of_Schema( $schemas );
}
/** @param Schema[] $schemas */
public static function anyOf(array $schemas): Any_Of_Schema {
return new Any_Of_Schema($schemas);
}
/** @param Schema[] $schemas */
public static function anyOf( array $schemas ): Any_Of_Schema {
return new Any_Of_Schema( $schemas );
}
}

View File

@ -2,96 +2,95 @@
namespace MailPoet\EmailEditor\Validator;
use function json_encode;
use function rest_get_allowed_schema_keywords;
abstract class Schema {
protected $schema = [];
protected $schema = array();
/** @return static */
public function nullable() {
$type = $this->schema['type'] ?? ['null'];
return $this->updateSchemaProperty('type', is_array($type) ? $type : [$type, 'null']);
}
/** @return static */
public function nullable() {
$type = $this->schema['type'] ?? array( 'null' );
return $this->updateSchemaProperty( 'type', is_array( $type ) ? $type : array( $type, 'null' ) );
}
/** @return static */
public function nonNullable() {
$type = $this->schema['type'] ?? null;
return $type === null
? $this->unsetSchemaProperty('type')
: $this->updateSchemaProperty('type', is_array($type) ? $type[0] : $type);
}
/** @return static */
public function nonNullable() {
$type = $this->schema['type'] ?? null;
return $type === null
? $this->unsetSchemaProperty( 'type' )
: $this->updateSchemaProperty( 'type', is_array( $type ) ? $type[0] : $type );
}
/** @return static */
public function required() {
return $this->updateSchemaProperty('required', true);
}
/** @return static */
public function required() {
return $this->updateSchemaProperty( 'required', true );
}
/** @return static */
public function optional() {
return $this->unsetSchemaProperty('required');
}
/** @return static */
public function optional() {
return $this->unsetSchemaProperty( 'required' );
}
/** @return static */
public function title(string $title) {
return $this->updateSchemaProperty('title', $title);
}
/** @return static */
public function title( string $title ) {
return $this->updateSchemaProperty( 'title', $title );
}
/** @return static */
public function description(string $description) {
return $this->updateSchemaProperty('description', $description);
}
/** @return static */
public function description( string $description ) {
return $this->updateSchemaProperty( 'description', $description );
}
/** @return static */
public function default($default) {
return $this->updateSchemaProperty('default', $default);
}
/** @return static */
public function default( $default ) {
return $this->updateSchemaProperty( 'default', $default );
}
/** @return static */
public function field(string $name, $value) {
if (in_array($name, $this->getReservedKeywords(), true)) {
throw new \Exception("Field name '$name' is reserved");
}
return $this->updateSchemaProperty($name, $value);
}
/** @return static */
public function field( string $name, $value ) {
if ( in_array( $name, $this->getReservedKeywords(), true ) ) {
throw new \Exception( "Field name '$name' is reserved" );
}
return $this->updateSchemaProperty( $name, $value );
}
public function toArray(): array {
return $this->schema;
}
public function toArray(): array {
return $this->schema;
}
public function toString(): string {
$json = json_encode($this->schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
$error = json_last_error();
if ($error || $json === false) {
throw new \Exception(json_last_error_msg(), (string)$error);
}
return $json;
}
public function toString(): string {
$json = json_encode( $this->schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION );
$error = json_last_error();
if ( $error || $json === false ) {
throw new \Exception( json_last_error_msg(), (string) $error );
}
return $json;
}
/** @return static */
protected function updateSchemaProperty(string $name, $value) {
$clone = clone $this;
$clone->schema[$name] = $value;
return $clone;
}
/** @return static */
protected function updateSchemaProperty( string $name, $value ) {
$clone = clone $this;
$clone->schema[ $name ] = $value;
return $clone;
}
/** @return static */
protected function unsetSchemaProperty(string $name) {
$clone = clone $this;
unset($clone->schema[$name]);
return $clone;
}
/** @return static */
protected function unsetSchemaProperty( string $name ) {
$clone = clone $this;
unset( $clone->schema[ $name ] );
return $clone;
}
protected function getReservedKeywords(): array {
return rest_get_allowed_schema_keywords();
}
protected function getReservedKeywords(): array {
return rest_get_allowed_schema_keywords();
}
protected function validatePattern(string $pattern): void {
$escaped = str_replace('#', '\\#', $pattern);
$regex = "#$escaped#u";
if (@preg_match($regex, '') === false) {
throw new \Exception("Invalid regular expression '$regex'");
}
}
protected function validatePattern( string $pattern ): void {
$escaped = str_replace( '#', '\\#', $pattern );
$regex = "#$escaped#u";
if ( @preg_match( $regex, '' ) === false ) {
throw new \Exception( "Invalid regular expression '$regex'" );
}
}
}

View File

@ -6,17 +6,17 @@ use MailPoet\UnexpectedValueException;
use WP_Error;
class Validation_Exception extends UnexpectedValueException {
/** @var WP_Error */
protected $wpError;
/** @var WP_Error */
protected $wpError;
public static function createFromWpError(WP_Error $wpError): self {
$exception = self::create()
->withMessage($wpError->get_error_message());
$exception->wpError = $wpError;
return $exception;
}
public static function createFromWpError( WP_Error $wpError ): self {
$exception = self::create()
->withMessage( $wpError->get_error_message() );
$exception->wpError = $wpError;
return $exception;
}
public function getWpError(): WP_Error {
return $this->wpError;
}
public function getWpError(): WP_Error {
return $this->wpError;
}
}

View File

@ -8,202 +8,210 @@ use stdClass;
use WP_Error;
class Validator {
/** @var WPFunctions */
private $wp;
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
/**
* Strict validation & sanitization implementation.
* It only coerces int to float (e.g. 5 to 5.0).
*
* @param mixed $value
* @return mixed
*/
public function validate(Schema $schema, $value, string $paramName = 'value') {
return $this->validateSchemaArray($schema->toArray(), $value, $paramName);
}
/**
* Strict validation & sanitization implementation.
* It only coerces int to float (e.g. 5 to 5.0).
*
* @param mixed $value
* @return mixed
*/
public function validate( Schema $schema, $value, string $paramName = 'value' ) {
return $this->validateSchemaArray( $schema->toArray(), $value, $paramName );
}
/**
* Strict validation & sanitization implementation.
* It only coerces int to float (e.g. 5 to 5.0).
*
* @param array $schema. The array must follow the format, which is returned from Schema::toArray().
* @param mixed $value
* @return mixed
*/
public function validateSchemaArray(array $schema, $value, string $paramName = 'value') {
$result = $this->validateAndSanitizeValueFromSchema($value, $schema, $paramName);
if ($result instanceof WP_Error) {
throw Validation_Exception::createFromWpError($result);
}
return $result;
}
/**
* Strict validation & sanitization implementation.
* It only coerces int to float (e.g. 5 to 5.0).
*
* @param array $schema. The array must follow the format, which is returned from Schema::toArray().
* @param mixed $value
* @return mixed
*/
public function validateSchemaArray( array $schema, $value, string $paramName = 'value' ) {
$result = $this->validateAndSanitizeValueFromSchema( $value, $schema, $paramName );
if ( $result instanceof WP_Error ) {
throw Validation_Exception::createFromWpError( $result );
}
return $result;
}
/**
* Mirrors rest_validate_value_from_schema() and rest_sanitize_value_from_schema().
*
* @param mixed $value
* @param array $schema
* @param string $paramName
* @return mixed|WP_Error
*/
private function validateAndSanitizeValueFromSchema($value, array $schema, string $paramName) {
// nullable
$fullType = $schema['type'] ?? null;
if (is_array($fullType) && in_array('null', $fullType, true) && $value === null) {
return null;
}
/**
* Mirrors rest_validate_value_from_schema() and rest_sanitize_value_from_schema().
*
* @param mixed $value
* @param array $schema
* @param string $paramName
* @return mixed|WP_Error
*/
private function validateAndSanitizeValueFromSchema( $value, array $schema, string $paramName ) {
// nullable
$fullType = $schema['type'] ?? null;
if ( is_array( $fullType ) && in_array( 'null', $fullType, true ) && $value === null ) {
return null;
}
// anyOf, oneOf
if (isset($schema['anyOf'])) {
return $this->validateAndSanitizeAnyOf($value, $schema, $paramName);
} elseif (isset($schema['oneOf'])) {
return $this->validateAndSanitizeOneOf($value, $schema, $paramName);
}
// anyOf, oneOf
if ( isset( $schema['anyOf'] ) ) {
return $this->validateAndSanitizeAnyOf( $value, $schema, $paramName );
} elseif ( isset( $schema['oneOf'] ) ) {
return $this->validateAndSanitizeOneOf( $value, $schema, $paramName );
}
// make types strict
$type = is_array($fullType) ? $fullType[0] : $fullType;
switch ($type) {
case 'number':
if (!is_float($value) && !is_int($value)) {
return $this->getTypeError($paramName, $fullType);
}
break;
case 'integer':
if (!is_int($value)) {
return $this->getTypeError($paramName, $fullType);
}
break;
case 'boolean':
if (!is_bool($value)) {
return $this->getTypeError($paramName, $fullType);
}
break;
case 'array':
if (!is_array($value)) {
return $this->getTypeError($paramName, $fullType);
}
// make types strict
$type = is_array( $fullType ) ? $fullType[0] : $fullType;
switch ( $type ) {
case 'number':
if ( ! is_float( $value ) && ! is_int( $value ) ) {
return $this->getTypeError( $paramName, $fullType );
}
break;
case 'integer':
if ( ! is_int( $value ) ) {
return $this->getTypeError( $paramName, $fullType );
}
break;
case 'boolean':
if ( ! is_bool( $value ) ) {
return $this->getTypeError( $paramName, $fullType );
}
break;
case 'array':
if ( ! is_array( $value ) ) {
return $this->getTypeError( $paramName, $fullType );
}
if (isset($schema['items'])) {
foreach ($value as $i => $v) {
$result = $this->validateAndSanitizeValueFromSchema($v, $schema['items'], $paramName . '[' . $i . ']');
if ($this->wp->isWpError($result)) {
return $result;
}
}
}
break;
case 'object':
if (!is_array($value) && !$value instanceof stdClass && !$value instanceof JsonSerializable) {
return $this->getTypeError($paramName, $fullType);
}
if ( isset( $schema['items'] ) ) {
foreach ( $value as $i => $v ) {
$result = $this->validateAndSanitizeValueFromSchema( $v, $schema['items'], $paramName . '[' . $i . ']' );
if ( $this->wp->isWpError( $result ) ) {
return $result;
}
}
}
break;
case 'object':
if ( ! is_array( $value ) && ! $value instanceof stdClass && ! $value instanceof JsonSerializable ) {
return $this->getTypeError( $paramName, $fullType );
}
// ensure string keys
$value = (array)($value instanceof JsonSerializable ? $value->jsonSerialize() : $value);
if (count(array_filter(array_keys($value), 'is_string')) !== count($value)) {
return $this->getTypeError($paramName, $fullType);
}
// ensure string keys
$value = (array) ( $value instanceof JsonSerializable ? $value->jsonSerialize() : $value );
if ( count( array_filter( array_keys( $value ), 'is_string' ) ) !== count( $value ) ) {
return $this->getTypeError( $paramName, $fullType );
}
// validate object properties
foreach ($value as $k => $v) {
if (isset($schema['properties'][$k])) {
$result = $this->validateAndSanitizeValueFromSchema($v, $schema['properties'][$k], $paramName . '[' . $k . ']');
if ($this->wp->isWpError($result)) {
return $result;
}
continue;
}
// validate object properties
foreach ( $value as $k => $v ) {
if ( isset( $schema['properties'][ $k ] ) ) {
$result = $this->validateAndSanitizeValueFromSchema( $v, $schema['properties'][ $k ], $paramName . '[' . $k . ']' );
if ( $this->wp->isWpError( $result ) ) {
return $result;
}
continue;
}
$patternPropertySchema = $this->wp->restFindMatchingPatternPropertySchema($k, $schema);
if ($patternPropertySchema) {
$result = $this->validateAndSanitizeValueFromSchema($v, $patternPropertySchema, $paramName . '[' . $k . ']');
if ($this->wp->isWpError($result)) {
return $result;
}
continue;
}
$patternPropertySchema = $this->wp->restFindMatchingPatternPropertySchema( $k, $schema );
if ( $patternPropertySchema ) {
$result = $this->validateAndSanitizeValueFromSchema( $v, $patternPropertySchema, $paramName . '[' . $k . ']' );
if ( $this->wp->isWpError( $result ) ) {
return $result;
}
continue;
}
if (isset($schema['additionalProperties']) && is_array($schema['additionalProperties'])) {
$result = $this->validateAndSanitizeValueFromSchema($v, $schema['additionalProperties'], $paramName . '[' . $k . ']');
if ($this->wp->isWpError($result)) {
return $result;
}
}
}
break;
}
if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
$result = $this->validateAndSanitizeValueFromSchema( $v, $schema['additionalProperties'], $paramName . '[' . $k . ']' );
if ( $this->wp->isWpError( $result ) ) {
return $result;
}
}
}
break;
}
$result = $this->wp->restValidateValueFromSchema($value, $schema, $paramName);
if ($this->wp->isWpError($result)) {
return $result;
}
return $this->wp->restSanitizeValueFromSchema($value, $schema, $paramName);
}
$result = $this->wp->restValidateValueFromSchema( $value, $schema, $paramName );
if ( $this->wp->isWpError( $result ) ) {
return $result;
}
return $this->wp->restSanitizeValueFromSchema( $value, $schema, $paramName );
}
/**
* Mirrors rest_find_any_matching_schema().
*
* @param mixed $value
* @return mixed|WP_Error
*/
private function validateAndSanitizeAnyOf($value, array $anyOfSchema, string $paramName) {
$errors = [];
foreach ($anyOfSchema['anyOf'] as $index => $schema) {
$result = $this->validateAndSanitizeValueFromSchema($value, $schema, $paramName);
if (!$this->wp->isWpError($result)) {
return $result;
}
$errors[] = ['error_object' => $result, 'schema' => $schema, 'index' => $index];
}
return $this->wp->restGetCombiningOperationError($value, $paramName, $errors);
}
/**
* Mirrors rest_find_any_matching_schema().
*
* @param mixed $value
* @return mixed|WP_Error
*/
private function validateAndSanitizeAnyOf( $value, array $anyOfSchema, string $paramName ) {
$errors = array();
foreach ( $anyOfSchema['anyOf'] as $index => $schema ) {
$result = $this->validateAndSanitizeValueFromSchema( $value, $schema, $paramName );
if ( ! $this->wp->isWpError( $result ) ) {
return $result;
}
$errors[] = array(
'error_object' => $result,
'schema' => $schema,
'index' => $index,
);
}
return $this->wp->restGetCombiningOperationError( $value, $paramName, $errors );
}
/**
* Mirrors rest_find_one_matching_schema().
*
* @param mixed $value
* @return mixed|WP_Error
*/
private function validateAndSanitizeOneOf($value, array $oneOfSchema, string $paramName) {
$matchingSchemas = [];
$errors = [];
$data = null;
foreach ($oneOfSchema['oneOf'] as $index => $schema) {
$result = $this->validateAndSanitizeValueFromSchema($value, $schema, $paramName);
if ($this->wp->isWpError($result)) {
$errors[] = ['error_object' => $result, 'schema' => $schema, 'index' => $index];
} else {
$data = $result;
$matchingSchemas[$index] = $schema;
}
}
/**
* Mirrors rest_find_one_matching_schema().
*
* @param mixed $value
* @return mixed|WP_Error
*/
private function validateAndSanitizeOneOf( $value, array $oneOfSchema, string $paramName ) {
$matchingSchemas = array();
$errors = array();
$data = null;
foreach ( $oneOfSchema['oneOf'] as $index => $schema ) {
$result = $this->validateAndSanitizeValueFromSchema( $value, $schema, $paramName );
if ( $this->wp->isWpError( $result ) ) {
$errors[] = array(
'error_object' => $result,
'schema' => $schema,
'index' => $index,
);
} else {
$data = $result;
$matchingSchemas[ $index ] = $schema;
}
}
if (!$matchingSchemas) {
return $this->wp->restGetCombiningOperationError($value, $paramName, $errors);
}
if ( ! $matchingSchemas ) {
return $this->wp->restGetCombiningOperationError( $value, $paramName, $errors );
}
if (count($matchingSchemas) > 1) {
// reuse WP method to generate detailed error
$invalidSchema = ['type' => []];
$oneOf = array_replace(array_fill(0, count($oneOfSchema['oneOf']), $invalidSchema), $matchingSchemas);
return $this->wp->restFindOneMatchingSchema($value, ['oneOf' => $oneOf], $paramName);
}
return $data;
}
if ( count( $matchingSchemas ) > 1 ) {
// reuse WP method to generate detailed error
$invalidSchema = array( 'type' => array() );
$oneOf = array_replace( array_fill( 0, count( $oneOfSchema['oneOf'] ), $invalidSchema ), $matchingSchemas );
return $this->wp->restFindOneMatchingSchema( $value, array( 'oneOf' => $oneOf ), $paramName );
}
return $data;
}
/** @param string|string[] $type */
private function getTypeError(string $param, $type): WP_Error {
$type = is_array($type) ? $type : [$type];
return new WP_Error(
'rest_invalid_type',
// translators: %1$s is the current parameter and %2$s a comma-separated list of the allowed types.
sprintf(__('%1$s is not of type %2$s.', 'mailpoet'), $param, implode(',', $type)),
['param' => $param]
);
}
/** @param string|string[] $type */
private function getTypeError( string $param, $type ): WP_Error {
$type = is_array( $type ) ? $type : array( $type );
return new WP_Error(
'rest_invalid_type',
// translators: %1$s is the current parameter and %2$s a comma-separated list of the allowed types.
sprintf( __( '%1$s is not of type %2$s.', 'mailpoet' ), $param, implode( ',', $type ) ),
array( 'param' => $param )
);
}
}

View File

@ -3,31 +3,31 @@
namespace MailPoet\EmailEditor;
class Container {
protected array $services = [];
protected array $instances = [];
protected array $services = array();
protected array $instances = array();
public function set(string $name, callable $callable): void {
$this->services[$name] = $callable;
}
public function set( string $name, callable $callable ): void {
$this->services[ $name ] = $callable;
}
/**
* @template T
* @param class-string<T> $name
* @return T
*/
public function get(string $name) {
// Check if the service is already instantiated
if (isset($this->instances[$name])) {
return $this->instances[$name];
}
/**
* @template T
* @param class-string<T> $name
* @return T
*/
public function get( string $name ) {
// Check if the service is already instantiated
if ( isset( $this->instances[ $name ] ) ) {
return $this->instances[ $name ];
}
// Check if the service is registered
if (!isset($this->services[$name])) {
throw new \Exception("Service not found: $name");
}
// Check if the service is registered
if ( ! isset( $this->services[ $name ] ) ) {
throw new \Exception( "Service not found: $name" );
}
$this->instances[$name] = $this->services[$name]($this);
$this->instances[ $name ] = $this->services[ $name ]( $this );
return $this->instances[$name];
}
return $this->instances[ $name ];
}
}