diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php index 24201e82fd..3b2dcb1237 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php @@ -180,6 +180,39 @@ final class WP_Interactivity_API_Directives_Processor extends WP_HTML_Tag_Proces return array( $opener_tag, $closer_tag ); } + /** + * Skips processing the content between tags. + * + * It positions the cursor in the closer tag of the foreign element, if it + * exists. + * + * This function is intended to skip processing SVG and MathML inner content + * instead of bailing out the whole processing. + * + * @since 6.5.0 + * + * @access private + * + * @return bool Whether the foreign content was successfully skipped. + */ + public function skip_to_tag_closer(): bool { + $depth = 1; + $tag_name = $this->get_tag(); + while ( $depth > 0 && $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) ) { + if ( $this->has_self_closing_flag() ) { + continue; + } + $depth += $this->is_tag_closer() ? -1 : 1; + } + + return 0 === $depth; + } + /** * Finds the matching closing tag for an opening tag. * diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index 536b6623e2..9e5b1be1fa 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -235,9 +235,14 @@ final class WP_Interactivity_API { while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { $tag_name = $p->get_tag(); + /* + * Directives inside SVG and MATH tags are not processed, + * as they are not compatible with the Tag Processor yet. + * We still process the rest of the HTML. + */ if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) { - $unbalanced = true; - break; + $p->skip_to_tag_closer(); + continue; } if ( $p->is_tag_closer() ) { diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index 5e8298a77e..25b8379acd 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -11,7 +11,7 @@ * * @coversDefaultClass WP_Interactivity_API */ -class Tests_WP_Interactivity_API extends WP_UnitTestCase { +class Tests_Interactivity_API_WpInteractivityAPI extends WP_UnitTestCase { /** * Instance of WP_Interactivity_API. * @@ -508,50 +508,131 @@ class Tests_WP_Interactivity_API extends WP_UnitTestCase { } /** - * Tests that the `process_directives` returns the same HTML if it finds an - * SVG tag. + * Tests that the `process_directives` process the HTML outside a SVG tag. * - * @ticket 60356 + * @ticket 60517 * * @covers ::process_directives */ - public function test_process_directives_doesnt_change_html_if_contains_svgs() { - $this->interactivity->state( 'myPlugin', array( 'id' => 'some-id' ) ); + public function test_process_directives_changes_html_if_contains_svgs() { + $this->interactivity->state( + 'myPlugin', + array( + 'id' => 'some-id', + 'width' => '100', + ) + ); $html = ' -
- +
+ + Red Circle - -
+ +
+
+ '; $processed_html = $this->interactivity->process_directives( $html ); $p = new WP_HTML_Tag_Processor( $processed_html ); - $p->next_tag(); + $p->next_tag( 'svg' ); + $this->assertNull( $p->get_attribute( 'width' ) ); + $p->next_tag( 'div' ); + $this->assertEquals( 'some-id', $p->get_attribute( 'id' ) ); + $p->next_tag( 'div' ); + $this->assertEquals( '100', $p->get_attribute( 'id' ) ); + } + + /** + * Tests that the `process_directives` does not process the HTML + * inside SVG tags. + * + * @ticket 60517 + * + * @covers ::process_directives + */ + public function test_process_directives_does_not_change_inner_html_in_svgs() { + $this->interactivity->state( + 'myPlugin', + array( + 'id' => 'some-id', + ) + ); + $html = ' +
+ + + + +
+ '; + $processed_html = $this->interactivity->process_directives( $html ); + $p = new WP_HTML_Tag_Processor( $processed_html ); + $p->next_tag( 'div' ); $this->assertNull( $p->get_attribute( 'id' ) ); } /** - * Tests that the `process_directives` returns the same HTML if it finds an + * Tests that the `process_directives` process the HTML outside the * MathML tag. * - * @ticket 60356 + * @ticket 60517 * * @covers ::process_directives */ - public function test_process_directives_doesnt_change_html_if_contains_math() { - $this->interactivity->state( 'myPlugin', array( 'id' => 'some-id' ) ); + public function test_process_directives_change_html_if_contains_math() { + $this->interactivity->state( + 'myPlugin', + array( + 'id' => 'some-id', + 'math' => 'ml-id', + ) + ); $html = ' -
- +
+ x = 1 -
+
+ '; $processed_html = $this->interactivity->process_directives( $html ); $p = new WP_HTML_Tag_Processor( $processed_html ); - $p->next_tag(); + $p->next_tag( 'math' ); + $this->assertNull( $p->get_attribute( 'id' ) ); + $p->next_tag( 'div' ); + $this->assertEquals( 'some-id', $p->get_attribute( 'id' ) ); + } + + /** + * Tests that the `process_directives` does not process the HTML + * inside MathML tags. + * + * @ticket 60517 + * + * @covers ::process_directives + */ + public function test_process_directives_does_not_change_inner_html_in_math() { + $this->interactivity->state( + 'myPlugin', + array( + 'id' => 'some-id', + ) + ); + $html = ' +
+ + + x + = + 1 + +
+ '; + $processed_html = $this->interactivity->process_directives( $html ); + $p = new WP_HTML_Tag_Processor( $processed_html ); + $p->next_tag( 'div' ); $this->assertNull( $p->get_attribute( 'id' ) ); } diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPIDirectivesProcessor.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPIDirectivesProcessor.php index bf2225597c..e7395b5f38 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPIDirectivesProcessor.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPIDirectivesProcessor.php @@ -11,7 +11,7 @@ * * @coversDefaultClass WP_Interactivity_API_Directives_Processor */ -class Tests_WP_Interactivity_API_Directives_Processor extends WP_UnitTestCase { +class Tests_Interactivity_API_WpInteractivityAPIDirectivesProcessor extends WP_UnitTestCase { /** * Tests the `get_content_between_balanced_template_tags` method on template * tags. @@ -778,4 +778,51 @@ class Tests_WP_Interactivity_API_Directives_Processor extends WP_UnitTestCase { $p->next_tag( array( 'tag_closers' => 'visit' ) ); $this->assertFalse( $p->next_balanced_tag_closer_tag() ); } + + /** + * Tests that skip_to_tag_closer skips to the next tag, + * independant of the content. + * + * @ticket 60517 + * + * @covers ::skip_to_tag_closer + */ + public function test_skip_to_tag_closer() { + $content = '
Not closed
'; + $p = new WP_Interactivity_API_Directives_Processor( $content ); + $p->next_tag(); + $this->assertTrue( $p->skip_to_tag_closer() ); + $this->assertTrue( $p->is_tag_closer() ); + $this->assertEquals( 'DIV', $p->get_tag() ); + } + + /** + * Tests that skip_to_tag_closer does not skip to the + * next tag if there is no closing tag. + * + * @ticket 60517 + * + * @covers ::skip_to_tag_closer + */ + public function test_skip_to_tag_closer_bails_not_closed() { + $content = '
Not closed parent'; + $p = new WP_Interactivity_API_Directives_Processor( $content ); + $p->next_tag(); + $this->assertFalse( $p->skip_to_tag_closer() ); + } + + /** + * Tests that skip_to_tag_closer does not skip to the next + * tag if the closing tag is different from the current tag. + * + * @ticket 60517 + * + * @covers ::skip_to_tag_closer + */ + public function test_skip_to_tag_closer_bails_different_tags() { + $content = '
'; + $p = new WP_Interactivity_API_Directives_Processor( $content ); + $p->next_tag(); + $this->assertFalse( $p->skip_to_tag_closer() ); + } }