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 );
 	}

 	/**