Skip to content

Commit 3440ab2

Browse files
committed
Tests: Expand HTML character reference decoding coverage.
Add named and numeric character reference cases for `WP_HTML_Decoder`, including semicolon handling, case sensitivity, Unicode boundaries, and Windows-1252 remapping. Generate numeric syntax variants with independent, literal expected strings. Developed in: #10810 Props huzaifaalmesbah, dmsnell. See #61072. git-svn-id: https://develop.svn.wordpress.org/trunk@64286 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 0eeb51c commit 3440ab2

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

‎tests/phpunit/tests/html-api/wpHtmlDecoder.php‎

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,43 @@ public static function data_semicolonless_attribute_behaviors(): array {
246246
);
247247
}
248248

249+
/**
250+
* Ensures proper decoding of ambiguous ampersands.
251+
*
252+
* @ticket 61072
253+
*
254+
* @dataProvider data_ambiguous_ampersands
255+
*
256+
* @param string $context 'attribute' or 'data'.
257+
* @param string $raw_text Raw text.
258+
* @param string $expected Expected decoded string.
259+
*/
260+
public function test_decodes_ambiguous_ampersands( string $context, string $raw_text, string $expected ): void {
261+
$this->assertSame(
262+
$expected,
263+
WP_HTML_Decoder::decode( $context, $raw_text ),
264+
"Failed handling ambiguous ampersand in context '{$context}' for '{$raw_text}'."
265+
);
266+
}
267+
268+
/**
269+
* Data provider for ambiguous ampersands.
270+
*
271+
* @return array<string, array{string, string, string}>
272+
*/
273+
public static function data_ambiguous_ampersands(): array {
274+
return array(
275+
'Semicolonless reference (data)' => array( 'data', '&amp', '&' ),
276+
'Semicolonless reference (attr)' => array( 'attribute', '&amp', '&' ),
277+
'Semicolonless followed by equals (data)' => array( 'data', '&not=', '¬=' ),
278+
'Semicolonless followed by letter (data)' => array( 'data', '&notit', '¬it' ),
279+
'With semicolon (data)' => array( 'data', '&not;', '¬' ),
280+
'With semicolon (attr)' => array( 'attribute', '&not;', '¬' ),
281+
'Semicolonless followed by space (data)' => array( 'data', '&not ', '¬ ' ),
282+
'Semicolonless followed by space (attr)' => array( 'attribute', '&not ', '¬ ' ),
283+
);
284+
}
285+
249286
/**
250287
* Ensures ambiguous ampersand is recognized with trailing ASCII alphanumerics.
251288
*
@@ -517,4 +554,200 @@ public static function data_attributes_with_prefix_and_case_sensitive_match() {
517554
array( 'http://wordpress.org', 'https', 'ascii-case-insensitive', false ),
518555
);
519556
}
557+
558+
/**
559+
* Ensures decoding of named character references in attributes.
560+
*
561+
* @ticket 61072
562+
*
563+
* @dataProvider data_decode_attribute_named_character_references
564+
*
565+
* @param string $raw_text Raw attribute value containing named character reference.
566+
* @param string $expected Expected decoded string.
567+
*/
568+
public function test_decode_attribute_decodes_named_character_references( string $raw_text, string $expected ): void {
569+
$this->assertSame(
570+
$expected,
571+
WP_HTML_Decoder::decode_attribute( $raw_text ),
572+
"Failed decoding named character reference in attribute '{$raw_text}'."
573+
);
574+
}
575+
576+
/**
577+
* Data provider for named character references in attributes.
578+
*
579+
* Beyond the main syntax characters, covers references which do and do not
580+
* decode without a trailing semicolon, as well as case-sensitive names.
581+
*
582+
* @return array<string, array{string, string}>
583+
*/
584+
public static function data_decode_attribute_named_character_references(): array {
585+
return array(
586+
// The main syntax characters.
587+
'Ampersand with semicolon' => array( '&amp;', '&' ),
588+
'Ampersand without semicolon' => array( '&amp', '&' ),
589+
'Less-than with semicolon' => array( '&lt;', '<' ),
590+
'Less-than without semicolon' => array( '&lt', '<' ),
591+
'Greater-than with semicolon' => array( '&gt;', '>' ),
592+
'Greater-than without semicolon' => array( '&gt', '>' ),
593+
'Double quote with semicolon' => array( '&quot;', '"' ),
594+
'Double quote without semicolon' => array( '&quot', '"' ),
595+
596+
// Legacy references which decode without a semicolon.
597+
'Copyright with semicolon' => array( '&copy;', '©' ),
598+
'Copyright without semicolon' => array( '&copy', '©' ),
599+
'Uppercase copyright with semicolon' => array( '&COPY;', '©' ),
600+
'Uppercase copyright without semicolon' => array( '&COPY', '©' ),
601+
602+
// References which only decode with a semicolon.
603+
'C with dot with semicolon' => array( '&cdot;', 'ċ' ),
604+
'C with dot without semicolon' => array( '&cdot', '&cdot' ),
605+
'Uppercase C with dot with semicolon' => array( '&Cdot;', 'Ċ' ),
606+
'Uppercase C with dot without semicolon' => array( '&Cdot', '&Cdot' ),
607+
'Backprime with semicolon' => array( '&backprime;', '‵' ),
608+
'Backprime without semicolon' => array( '&backprime', '&backprime' ),
609+
610+
// Names are case-sensitive: only the lowercase form exists.
611+
'Non-existent uppercase Backprime' => array( '&Backprime;', '&Backprime;' ),
612+
);
613+
}
614+
615+
/**
616+
* Ensures decoding of numeric character references across their syntactic variants.
617+
*
618+
* @ticket 61072
619+
*
620+
* @dataProvider data_numeric_character_references
621+
*
622+
* @param string $raw_text Raw numeric character reference.
623+
* @param string $expected Expected decoded string.
624+
*/
625+
public function test_decodes_numeric_character_references( string $raw_text, string $expected ): void {
626+
$this->assertSame(
627+
$expected,
628+
WP_HTML_Decoder::decode_text_node( $raw_text ),
629+
"Failed decoding numeric character reference in text node: '{$raw_text}'."
630+
);
631+
$this->assertSame(
632+
$expected,
633+
WP_HTML_Decoder::decode_attribute( $raw_text ),
634+
"Failed decoding numeric character reference in attribute: '{$raw_text}'."
635+
);
636+
}
637+
638+
/**
639+
* Data provider.
640+
*
641+
* Generates, for a representative set of code points, every combination of:
642+
* - decimal vs. hexadecimal syntax,
643+
* - `x` vs. `X` introducer and lower vs. upper case hexadecimal digits,
644+
* - count of leading zeros (which do not count toward the digit limit),
645+
* - semicolon present or missing.
646+
*
647+
* @return Generator<string, array{string, string}> Test cases.
648+
*/
649+
public static function data_numeric_character_references(): Generator {
650+
$code_points = array(
651+
'ASCII' => array( 0x41, 'A' ),
652+
'Two-byte UTF-8' => array( 0xE9, 'é' ),
653+
'Three-byte UTF-8' => array( 0x2603, '☃' ),
654+
'Astral plane' => array( 0x1F600, '😀' ),
655+
'Maximum code point' => array( 0x10FFFF, "\u{10FFFF}" ),
656+
657+
// Surrogate halves and code points beyond U+10FFFF decode to U+FFFD.
658+
'High surrogate start' => array( 0xD800, "\u{FFFD}" ),
659+
'High surrogate middle' => array( 0xDABC, "\u{FFFD}" ),
660+
'Low surrogate end' => array( 0xDFFF, "\u{FFFD}" ),
661+
'Out of range' => array( 0x110000, "\u{FFFD}" ),
662+
);
663+
664+
foreach ( $code_points as $label => list( $code_point, $expected ) ) {
665+
$hex = dechex( $code_point );
666+
$syntaxes = array(
667+
array( '#', (string) $code_point ),
668+
array( '#x', $hex ),
669+
array( '#x', strtoupper( $hex ) ),
670+
array( '#X', $hex ),
671+
array( '#X', strtoupper( $hex ) ),
672+
);
673+
674+
// Hexadecimal digits without letters have no case variants.
675+
$syntaxes = array_unique( $syntaxes, SORT_REGULAR );
676+
677+
foreach ( $syntaxes as list( $introducer, $digit_string ) ) {
678+
foreach ( array( 0, 1, 10 ) as $zero_count ) {
679+
foreach ( array( ';', '' ) as $terminator ) {
680+
$raw_text = '&' . $introducer . str_repeat( '0', $zero_count ) . $digit_string . $terminator;
681+
682+
yield "{$label}: {$raw_text}" => array( $raw_text, $expected );
683+
}
684+
}
685+
}
686+
}
687+
}
688+
689+
/**
690+
* Ensures that Windows-1252 mapped characters are properly decoded.
691+
*
692+
* @ticket 61072
693+
*
694+
* @dataProvider data_windows_1252_mapped_characters
695+
*
696+
* @param string $raw_text Raw numeric character reference.
697+
* @param string $expected Expected decoded character.
698+
*/
699+
public function test_decodes_windows_1252_mapped_characters( string $raw_text, string $expected ): void {
700+
$this->assertSame(
701+
$expected,
702+
WP_HTML_Decoder::decode_text_node( $raw_text ),
703+
"Failed decoding Windows-1252 character reference in text node: '{$raw_text}'."
704+
);
705+
$this->assertSame(
706+
$expected,
707+
WP_HTML_Decoder::decode_attribute( $raw_text ),
708+
"Failed decoding Windows-1252 character reference in attribute: '{$raw_text}'."
709+
);
710+
}
711+
712+
/**
713+
* Data provider for Windows-1252 mapped characters.
714+
*
715+
* @return array<string, array{string, string}>
716+
*/
717+
public static function data_windows_1252_mapped_characters(): array {
718+
return array(
719+
'Euro sign' => array( '&#x80;', '€' ),
720+
'Unmapped U+81' => array( '&#x81;', "\u{81}" ),
721+
'Single low-9' => array( '&#x82;', '‚' ),
722+
'F with hook' => array( '&#x83;', 'ƒ' ),
723+
'Double low-9' => array( '&#x84;', '„' ),
724+
'Ellipsis' => array( '&#x85;', '…' ),
725+
'Dagger' => array( '&#x86;', '†' ),
726+
'Double dagger' => array( '&#x87;', '‡' ),
727+
'Circumflex' => array( '&#x88;', 'ˆ' ),
728+
'Per mille' => array( '&#x89;', '‰' ),
729+
'S with caron' => array( '&#x8A;', 'Š' ),
730+
'Less single guil' => array( '&#x8B;', '‹' ),
731+
'OE ligature' => array( '&#x8C;', 'Œ' ),
732+
'Unmapped U+8D' => array( '&#x8D;', "\u{8D}" ),
733+
'Z with caron' => array( '&#x8E;', 'Ž' ),
734+
'Unmapped U+8F' => array( '&#x8F;', "\u{8F}" ),
735+
'Unmapped U+90' => array( '&#x90;', "\u{90}" ),
736+
'Left single quot' => array( '&#x91;', '‘' ),
737+
'Right single quo' => array( '&#x92;', '’' ),
738+
'Left double quot' => array( '&#x93;', '“' ),
739+
'Right double quo' => array( '&#x94;', '”' ),
740+
'Bullet' => array( '&#x95;', '•' ),
741+
'En dash' => array( '&#x96;', '–' ),
742+
'Em dash' => array( '&#x97;', '—' ),
743+
'Small tilde' => array( '&#x98;', '˜' ),
744+
'Trade mark' => array( '&#x99;', '™' ),
745+
's with caron' => array( '&#x9A;', 'š' ),
746+
'Right single gui' => array( '&#x9B;', '›' ),
747+
'oe ligature' => array( '&#x9C;', 'œ' ),
748+
'Unmapped U+9D' => array( '&#x9D;', "\u{9D}" ),
749+
'z with caron' => array( '&#x9E;', 'ž' ),
750+
'Y with diaeresis' => array( '&#x9F;', 'Ÿ' ),
751+
);
752+
}
520753
}

0 commit comments

Comments
 (0)