Commit 2b84595620b for woocommerce
commit 2b84595620b63ef162203ac823ca0dfae722d4ed
Author: Seun Olorunsola <30554163+triple0t@users.noreply.github.com>
Date: Mon May 11 13:59:27 2026 +0100
Fix block email editor dropping quote child blocks and duplicating cite (#64746)
* Fix block email editor dropping quote child blocks and duplicating cite
Quote::render_content used get_inner_content() with the default 'div' tag.
By the time it ran, every child block had already been wrapped in
<table>…<div class="email-block-layout">…</div>…</table> by the editor's
per-block render filter, so getElementsByTagName('div')->item(0) returned
only the first child's wrapper and dropped every subsequent paragraph.
Strip the <blockquote> tag and keep all its children. The visual quote
indent is provided by the email-block-quote table built in
get_block_wrapper(); keeping the inner blockquote produced a
quote-within-a-quote. Remove the cite element from the DOM after extracting
its text so it isn't rendered both inline and as the styled citation block.
Drop the now-unreachable RTL post-processor that ran a WP_HTML_Tag_Processor
against $quote_content looking for a blockquote tag — once the blockquote
is stripped, that branch always early-returned. The RTL right-side border
behavior remains, applied to the email-block-quote table in
get_block_wrapper().
Add integration coverage for multi-paragraph quotes, single-cite rendering,
and the no-blockquote-in-output invariant.
* Add changelog entry for quote renderer fix
diff --git a/packages/php/email-editor/changelog/stomail-8060-quote-child-blocks b/packages/php/email-editor/changelog/stomail-8060-quote-child-blocks
new file mode 100644
index 00000000000..dc4cb5424ca
--- /dev/null
+++ b/packages/php/email-editor/changelog/stomail-8060-quote-child-blocks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Block email editor: render every child block of a quote, render the citation only once, and stop emitting a quote-within-a-quote.
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-quote.php b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-quote.php
index 33eadc746eb..c8fda84f245 100644
--- a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-quote.php
+++ b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-quote.php
@@ -28,7 +28,8 @@ class Quote extends Abstract_Block_Renderer {
protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string {
$dom_helper = new Dom_Document_Helper( $block_content );
- // Extract citation if present.
+ // Extract citation if present, then remove it from the DOM so it isn't
+ // also rendered inline as part of the quote content below.
$citation_content = '';
$cite_element = $dom_helper->find_element( 'cite' );
if ( $cite_element ) {
@@ -37,73 +38,29 @@ class Quote extends Abstract_Block_Renderer {
$parsed_block,
$rendering_context
);
+ if ( $cite_element->parentNode ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+ $cite_element->parentNode->removeChild( $cite_element ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+ }
}
+ // Strip the <blockquote> tag and keep its children. The visual quote indent
+ // comes from the email-block-quote table built in get_block_wrapper(); keeping
+ // the original blockquote would render a double-bordered quote-within-a-quote.
+ // Children have already been re-wrapped in <table>…<div class="email-block-layout">…</div>…</table>
+ // by the email editor's per-block render filter, so passing 'div' (the previous default)
+ // would only return the first child's inner HTML and drop the rest.
+ $blockquote_element = $dom_helper->find_element( 'blockquote' );
+ $quote_content = $blockquote_element
+ ? $dom_helper->get_element_inner_html( $blockquote_element )
+ : $block_content;
+
return str_replace(
array( '{quote_content}', '{citation_content}' ),
- array( $this->get_quote_content( $block_content, $parsed_block, $rendering_context ), $citation_content ),
+ array( $quote_content, $citation_content ),
$this->get_block_wrapper( $block_content, $parsed_block, $rendering_context )
);
}
- /**
- * Get quote content with direction-aware default border overrides.
- *
- * @param string $block_content Block content.
- * @param array $parsed_block Parsed block.
- * @param Rendering_Context $rendering_context Rendering context.
- * @return string
- */
- private function get_quote_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string {
- $quote_content = $this->get_inner_content( $block_content );
-
- $original_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'blockquote', 'class' ) ?? '';
- $block_attributes = wp_parse_args(
- $parsed_block['attrs'] ?? array(),
- array(
- 'style' => array(),
- 'borderColor' => '',
- )
- );
-
- if ( ! $rendering_context->is_rtl() || $this->has_authored_border( $block_attributes ) ) {
- return $quote_content;
- }
-
- $authored_alignment = $this->get_authored_alignment( $block_attributes, $original_classname );
- if ( 'left' === $authored_alignment ) {
- return $quote_content;
- }
-
- $processor = new \WP_HTML_Tag_Processor( $quote_content );
- if ( ! $processor->next_tag( array( 'tag_name' => 'blockquote' ) ) ) {
- return $quote_content;
- }
-
- $style = $processor->get_attribute( 'style' );
- $style = is_string( $style ) ? rtrim( $style, ';' ) . ';' : '';
- $style .= \WP_Style_Engine::compile_css(
- 'center' === $authored_alignment
- ? array(
- 'border-left-style' => 'none',
- 'border-left-width' => '0',
- 'border-right-style' => 'none',
- 'border-right-width' => '0',
- )
- : array(
- 'border-left-style' => 'none',
- 'border-left-width' => '0',
- 'border-right-color' => 'currentColor',
- 'border-right-style' => 'solid',
- 'border-right-width' => '1px',
- ),
- ''
- );
- $processor->set_attribute( 'style', $style );
-
- return $processor->get_updated_html();
- }
-
/**
* Returns the citation content with a wrapper.
*
diff --git a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Quote_Test.php b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Quote_Test.php
index 77aebcc9ec4..bc7843048f7 100644
--- a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Quote_Test.php
+++ b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Quote_Test.php
@@ -76,8 +76,102 @@ class Quote_Test extends \Email_Editor_Integration_Test_Case {
$this->assertStringContainsString( 'Quote content', $rendered );
}
+ /**
+ * Test it renders every child block when the quote contains multiple paragraphs
+ * that have already been wrapped by the email editor's per-block renderer.
+ */
+ public function testItRendersAllChildBlocksWhenQuoteContainsMultipleParagraphs(): void {
+ // Mirrors the shape of $block_content received by Quote::render_content in
+ // production: each child paragraph has already gone through the email editor's
+ // render_block filter, which wraps it in a table + .email-block-layout div.
+ $block_content = <<<'HTML'
+<blockquote class="wp-block-quote">
+<table align="left" width="100%" style=""><tr><td><div class="email-block-layout" style=""><p>FIRST_CHILD_MARKER</p></div></td></tr></table>
+<table align="left" width="100%" style=""><tr><td><div class="email-block-layout" style=""><p>SECOND_CHILD_MARKER</p></div></td></tr></table>
+<table align="left" width="100%" style=""><tr><td><div class="email-block-layout" style=""><p>THIRD_CHILD_MARKER</p></div></td></tr></table>
+</blockquote>
+HTML;
+
+ $parsed_quote = array(
+ 'blockName' => 'core/quote',
+ 'attrs' => array(),
+ 'innerBlocks' => array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ 'innerHTML' => '<p>FIRST_CHILD_MARKER</p>',
+ 'innerContent' => array( '<p>FIRST_CHILD_MARKER</p>' ),
+ ),
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ 'innerHTML' => '<p>SECOND_CHILD_MARKER</p>',
+ 'innerContent' => array( '<p>SECOND_CHILD_MARKER</p>' ),
+ ),
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ 'innerHTML' => '<p>THIRD_CHILD_MARKER</p>',
+ 'innerContent' => array( '<p>THIRD_CHILD_MARKER</p>' ),
+ ),
+ ),
+ 'innerHTML' => '<blockquote class="wp-block-quote"></blockquote>',
+ 'innerContent' => array(
+ '<blockquote class="wp-block-quote">',
+ null,
+ null,
+ null,
+ '</blockquote>',
+ ),
+ );
+
+ $rendered = $this->quote_renderer->render( $block_content, $parsed_quote, $this->rendering_context );
+
+ $this->assertStringContainsString( 'FIRST_CHILD_MARKER', $rendered );
+ $this->assertStringContainsString( 'SECOND_CHILD_MARKER', $rendered );
+ $this->assertStringContainsString( 'THIRD_CHILD_MARKER', $rendered );
+
+ // The visual quote indent comes from the wrapping email-block-quote table;
+ // the inner <blockquote> must be stripped to avoid a quote-within-a-quote.
+ $this->assertStringNotContainsString( '<blockquote', $rendered );
+ }
+
+ /**
+ * Test it renders the citation exactly once when the quote contains a <cite>.
+ *
+ * The <cite> is rendered as a separate styled citation block; it must also be
+ * removed from the inline quote content so it does not appear twice.
+ */
+ public function testItRendersCitationOnlyOnce(): void {
+ $block_content = '<blockquote class="wp-block-quote"><p>Quote body</p><cite>UNIQUE_CITATION_TOKEN</cite></blockquote>';
+
+ $parsed_quote = $this->parsed_quote;
+ $parsed_quote['innerBlocks'] = array(
+ array(
+ 'blockName' => 'core/paragraph',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ 'innerHTML' => '<p>Quote body</p>',
+ 'innerContent' => array( '<p>Quote body</p>' ),
+ ),
+ );
+
+ $rendered = $this->quote_renderer->render( $block_content, $parsed_quote, $this->rendering_context );
+
+ $this->assertStringContainsString( 'UNIQUE_CITATION_TOKEN', $rendered );
+ $this->assertSame( 1, substr_count( $rendered, 'UNIQUE_CITATION_TOKEN' ), 'citation text appears more than once in rendered output' );
+ $this->assertStringContainsString( 'email-block-quote-citation', $rendered );
+ }
+
/**
* Test it moves default quote border to the right in RTL.
+ *
+ * The visual quote indent is provided by the email-block-quote table; the
+ * inner <blockquote> tag is stripped (see get_quote_content) so that the
+ * RTL right-side border on the table is the only border the recipient sees.
*/
public function testItUsesRightDefaultBorderInRtl(): void {
$theme_controller = $this->di_container->get( Theme_Controller::class );
@@ -88,9 +182,7 @@ class Quote_Test extends \Email_Editor_Integration_Test_Case {
$this->assertStringContainsString( 'border-width:0 1px 0 0;', $rendered );
$this->assertStringContainsString( 'border-style:solid;', $rendered );
- $this->assertStringContainsString( 'border-left-style:none;', $rendered );
- $this->assertStringContainsString( 'border-left-width:0;', $rendered );
- $this->assertStringContainsString( 'border-right-style:solid;', $rendered );
+ $this->assertStringNotContainsString( '<blockquote', $rendered );
}
/**