_export_data_grouped' );
+
+// src/wp-admin/widgets.php:24
+// Full paragraph with embedded documentation link
+wp_die( __( 'The theme you are currently using is not widget-aware, meaning that it has no sidebars that you are able to change. For information on making your theme widget-aware, please follow these instructions.' ) );
+
+// src/wp-admin/revision.php:158
+// Link in translation without placeholders
+$revisions_sidebar .= '' . __( 'Revisions Management' ) . '
'; + + +/** + * ============================================================================= + * PATTERN 2: sprintf with manual escaping (non-translation) + * ============================================================================= + * + * These patterns require developers to choose the correct escape function + * for each context (esc_url, esc_attr, esc_html). + */ + +// src/wp-includes/blocks/post-title.php:41 +// Link with multiple escaped attributes +$rel = ! empty( $attributes['rel'] ) ? 'rel="' . esc_attr( $attributes['rel'] ) . '"' : ''; +$title = sprintf( + '%4$s', + esc_url( get_the_permalink( $block->context['postId'] ) ), + esc_attr( $attributes['linkTarget'] ), + $rel, // Note: $rel already has esc_attr inside + $title // Note: $title escaping unclear +); + +// src/wp-includes/blocks/avatar.php:68 +// Similar link pattern for avatar +$avatar_block = sprintf( + '%4$s', + esc_url( get_author_posts_url( $author_id ) ), + esc_attr( $attributes['linkTarget'] ), + $label, // aria-label attribute, already escaped + $avatar_block // Inner HTML +); + +// src/wp-includes/formatting.php:3476 +// Image tag with esc_url and esc_attr +return sprintf( + 'Hi!
', + array(), + 'Hi!
', + ), + + 'basic text replacement' => array( + 'Hello, %name>!
', + array( 'name' => 'World' ), + 'Hello, World!
', + ), + + 'escapes special characters in text' => array( + 'Hello, %placeholder>!
', + array( 'placeholder' => 'Alice & Bob' ), + 'Hello, Alice & Bob!
', + ), + + 'escapes angle brackets in text' => array( + 'Hello, %name>!
', + array( 'name' => 'Hello, <little-bobby-tags>!
', + ), + + 'numeric placeholders' => array( + 'Hello, %0> and %1>!
', + array( 'Alice', 'Bob' ), + 'Hello, Alice and Bob!
', + ), + + 'repeated placeholders' => array( + '%0>, % 0 >, %name>, & %name>!
', + array( + 'Alice', + 'name' => 'Bob', + ), + 'Alice, Alice, Bob, & Bob!
', + ), + + 'nested template replacement' => array( + 'Hello, %html>', + array( 'html' => WP_HTML_Template::from( 'Alice & Bob' ) ), + '
Hello, Alice & Bob
', + ), + + 'replaces attribute values' => array( + '', + array( + 'n' => 'the name', + 'c' => 'the content', + ), + '', + ), + + 'escapes attribute values' => array( + '', + array( + 'c' => 'the "content" & whatever else', + ), + '', + ), + ); + } + + /** + * Test real-world patterns from WordPress core. + * + * @dataProvider data_real_world_examples + * + * @ticket 60229 + * + * @covers ::from + * @covers ::bind + * @covers ::render + */ + public function test_real_world_examples( string $template_string, array $replacements, string $expected ) { + $result = WP_HTML_Template::from( $template_string )->bind( $replacements )->render(); + $this->assertEqualHTML( $expected, $result ); + } + + /** + * Data provider with real-world patterns from WordPress core. + * + * Each test case is based on actual code patterns found in WordPress core + * that could benefit from the WP_HTML_Template API. + * + * @return array[] + */ + public static function data_real_world_examples() { + /* + * Group 1: Simple sprintf patterns with manual escaping. + * + * These patterns currently require developers to manually choose + * the correct escape function (esc_url, esc_attr, esc_html). + */ + + // src/wp-includes/formatting.php:3476 - Smiley image + yield 'formatting.php:3476 - smiley image tag' => array( + <<<'HTML' +
+ HTML,
+ );
+
+ // src/wp-includes/blocks/post-title.php:41 - Post title link
+ yield 'blocks/post-title.php:41 - post title link' => array(
+ '%title>',
+ array(
+ 'url' => 'https://example.com/hello-world/',
+ 'target' => '_blank',
+ 'title' => 'Hello World',
+ ),
+ 'Hello World',
+ );
+
+ // Same pattern with escaping needed
+ yield 'blocks/post-title.php:41 - post title link with special chars' => array(
+ "\n%title>\n",
+ array(
+ 'url' => 'https://example.com/hello-world/?foo=1&bar=2',
+ 'target' => '_blank',
+ 'title' => WP_HTML_Template::from( '\'%italic>\' & "%bold>"' )
+ ->bind(
+ array(
+ 'italic' => 'This',
+ 'bold' => 'That',
+ )
+ ),
+ ),
+ <<<'HTML'
+
+ 'This' & "That"
+
+ HTML,
+ );
+
+ /*
+ * Group 2: Translation patterns with embedded HTML.
+ *
+ * These patterns have HTML directly in translatable strings.
+ */
+
+ // src/wp-includes/functions.php:1620 - Error message (static, no placeholders)
+ yield 'functions.php:1620 - static error message' => array(
+ 'Error: This is not a valid feed template.',
+ array(),
+ 'Error: This is not a valid feed template.',
+ );
+
+ // src/wp-includes/functions.php:1844-1845 - Database repair link
+ yield 'functions.php:1844 - database repair link' => array(
+ <<<'HTML'
+ One or more database tables are unavailable. The database may need to be repaired.
+ HTML,
+ array(
+ 'url' => 'maint/repair.php?referrer=is_blog_installed',
+ ),
+ <<<'HTML'
+ One or more database tables are unavailable. The database may need to be repaired.
+ HTML,
+ );
+
+ // src/wp-admin/edit-form-advanced.php:185 - Scheduled post date
+ yield 'edit-form-advanced.php:185 - scheduled post date' => array(
+ 'Post scheduled for: %date>.',
+ array(
+ 'date' => 'March 15, 2025 at 10:30 am',
+ ),
+ 'Post scheduled for: March 15, 2025 at 10:30 am.',
+ );
+
+ // src/wp-includes/blocks/latest-posts.php:164-166 - Read more link with nested elements
+ yield 'blocks/latest-posts.php:164 - read more link with screen reader text' => array(
+ <<<'HTML'
+ … Read more: %title>
+ HTML,
+ array(
+ 'url' => 'https://example.com/my-post/',
+ 'title' => 'My Amazing Post',
+ ),
+ <<<'HTML'
+ … Read more: My Amazing Post
+ HTML,
+ );
+
+ // Same pattern with escaping needed
+ yield 'blocks/latest-posts.php:164 - read more with XSS attempt' => array(
+ <<<'HTML'
+ … Read more: %title>
+ HTML,
+ array(
+ 'url' => 'javascript:alert("xss")',
+ 'title' => '',
+ ),
+ <<<'HTML'
+ … Read more: <script>alert("xss")</script>
+ HTML,
+ );
+
+ // src/wp-includes/theme.php:978-979 - Theme error with name
+ yield 'theme.php:978 - theme error message' => array(
+ <<<'HTML'
+ Error: Current WordPress and PHP versions do not meet minimum requirements for %theme_name>.
+ HTML,
+ array(
+ 'theme_name' => 'Twenty Twenty-Five',
+ ),
+ <<<'HTML'
+ Error: Current WordPress and PHP versions do not meet minimum requirements for Twenty Twenty-Five.
+ HTML,
+ );
+
+ // src/wp-admin/includes/privacy-tools.php:404 - Code tag in error
+ yield 'privacy-tools.php:404 - code in error message' => array(
+ 'The %meta_key> post meta must be an array.',
+ array(
+ 'meta_key' => '_export_data_grouped',
+ ),
+ 'The _export_data_grouped post meta must be an array.',
+ );
+
+ /*
+ * Group 3: Edge cases.
+ */
+
+ // Placeholder reuse (same placeholder multiple times)
+ yield 'placeholder reuse' => array(
+ ' ',
+ array(
+ 'id' => 'user_name',
+ ),
+ ' ',
+ );
+
+ // Numeric placeholders like sprintf
+ yield 'numeric placeholders' => array(
+ '%1> by %3>',
+ array(
+ 'https://example.com/post/',
+ 'Post Title',
+ 'https://example.com/author/',
+ 'Author Name',
+ ),
+ 'Post Title by Author Name',
+ );
+
+ // Nested template (pre-escaped HTML)
+ yield 'nested template for complex structure' => array(
+ 'Hello%suffix>
', + array( + 'suffix' => '', + ), + 'Hello
', + ); + + // HTML entities in template (should be preserved) + yield 'HTML entities in template' => array( + '“%quote>”
', + array( + 'quote' => 'Hello World', + ), + '“Hello World”
', + ); + + // Multiple attributes on same element + yield 'multiple attributes on element' => array( + '', + array( + 'type' => 'text', + 'name' => 'user_email', + 'value' => 'test@example.com', + 'placeholder' => 'Enter your email', + ), + '', + ); + + // Attribute value with quotes and special characters + yield 'attribute with quotes and ampersands' => array( + 'Link', + array( + 'url' => 'https://example.com/?a=1&b=2', + 'title' => <<<'TEXT' + Click "here" for Tom & Jerry + TEXT, + ), + <<<'HTML' + Link + HTML, + ); + + // Self-closing void element + yield 'self-closing meta tag' => array( + '', + array( + 'name' => 'description', + 'content' => <<<'TEXT' + A page about "cats" & dogs + TEXT, + ), + <<<'HTML' + + HTML, + ); + + // src/wp-includes/blocks/avatar.php:68 - Complex link with aria-label + yield 'blocks/avatar.php:68 - avatar link' => array( + <<<'HTML' + %inner> + HTML, + array( + 'url' => 'https://example.com/author/johndoe/', + 'target' => '_blank', + 'aria_label' => '(John Doe author archive, opens in a new tab)', + 'inner' => WP_HTML_Template::from( '