PHP
6 days ago
LICENSE.md
6 days ago
README.md
6 days ago
class-wp-html-active-formatting-elements.php
6 days ago
class-wp-html-attribute-token.php
6 days ago
class-wp-html-decoder.php
6 days ago
class-wp-html-doctype-info.php
6 days ago
class-wp-html-native-processor-wrapper.php
6 days ago
class-wp-html-native-tag-processor-wrapper.php
6 days ago
class-wp-html-open-elements.php
6 days ago
class-wp-html-processor-state.php
6 days ago
class-wp-html-processor.php
6 days ago
class-wp-html-span.php
6 days ago
class-wp-html-stack-event.php
6 days ago
class-wp-html-tag-processor.php
6 days ago
class-wp-html-text-replacement.php
6 days ago
class-wp-html-token.php
6 days ago
class-wp-html-unsupported-exception.php
6 days ago
class-wp-token-map.php
6 days ago
composer.json
6 days ago
html5-named-character-references.php
6 days ago
class-wp-html-decoder.php
483 lines
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * HTML API: WP_HTML_Decoder class |
| 5 | * |
| 6 | * Decodes spans of raw text found inside HTML content. |
| 7 | * |
| 8 | * @package WordPress |
| 9 | * @subpackage HTML-API |
| 10 | * @since 6.6.0 |
| 11 | */ |
| 12 | class WP_HTML_Decoder { |
| 13 | /** |
| 14 | * Indicates if an attribute value starts with a given raw string value. |
| 15 | * |
| 16 | * Use this method to determine if an attribute value starts with a given string, regardless |
| 17 | * of how it might be encoded in HTML. For instance, `http:` could be represented as `http:` |
| 18 | * or as `http:` or as `http:` or as `http:`, or in many other ways. |
| 19 | * |
| 20 | * Example: |
| 21 | * |
| 22 | * $value = 'http://wordpress.org/'; |
| 23 | * true === WP_HTML_Decoder::attribute_starts_with( $value, 'http:', 'ascii-case-insensitive' ); |
| 24 | * false === WP_HTML_Decoder::attribute_starts_with( $value, 'https:', 'ascii-case-insensitive' ); |
| 25 | * |
| 26 | * @param string $haystack String containing the raw non-decoded attribute value. |
| 27 | * @param string $search_text Does the attribute value start with this plain string. |
| 28 | * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching. |
| 29 | * Default 'case-sensitive'. |
| 30 | * |
| 31 | * @return bool Whether the attribute value starts with the given string. |
| 32 | * @since 6.6.0 |
| 33 | */ |
| 34 | public static function attribute_starts_with( $haystack, $search_text, $case_sensitivity = 'case-sensitive' ): bool { |
| 35 | $search_length = strlen( $search_text ); |
| 36 | $loose_case = 'ascii-case-insensitive' === $case_sensitivity; |
| 37 | $haystack_end = strlen( $haystack ); |
| 38 | $search_at = 0; |
| 39 | $haystack_at = 0; |
| 40 | |
| 41 | while ( $search_at < $search_length && $haystack_at < $haystack_end ) { |
| 42 | $chars_match = $loose_case |
| 43 | ? strtolower( $haystack[ $haystack_at ] ) === strtolower( $search_text[ $search_at ] ) |
| 44 | : $haystack[ $haystack_at ] === $search_text[ $search_at ]; |
| 45 | |
| 46 | $is_introducer = '&' === $haystack[ $haystack_at ]; |
| 47 | $next_chunk = $is_introducer |
| 48 | ? self::read_character_reference( 'attribute', $haystack, $haystack_at, $token_length ) |
| 49 | : null; |
| 50 | |
| 51 | // If there's no character reference and the characters don't match, the match fails. |
| 52 | if ( null === $next_chunk && ! $chars_match ) { |
| 53 | return false; |
| 54 | } |
| 55 | |
| 56 | // If there's no character reference but the character do match, then it could still match. |
| 57 | if ( null === $next_chunk && $chars_match ) { |
| 58 | ++$haystack_at; |
| 59 | ++$search_at; |
| 60 | continue; |
| 61 | } |
| 62 | |
| 63 | // If there is a character reference, then the decoded value must exactly match what follows in the search string. |
| 64 | if ( 0 !== substr_compare( $search_text, $next_chunk, $search_at, strlen( $next_chunk ), $loose_case ) ) { |
| 65 | return false; |
| 66 | } |
| 67 | |
| 68 | // The character reference matched, so continue checking. |
| 69 | $haystack_at += $token_length; |
| 70 | $search_at += strlen( $next_chunk ); |
| 71 | } |
| 72 | |
| 73 | return true; |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * Returns a string containing the decoded value of a given HTML text node. |
| 78 | * |
| 79 | * Text nodes appear in HTML DATA sections, which are the text segments inside |
| 80 | * and around tags, excepting SCRIPT and STYLE elements (and some others), |
| 81 | * whose inner text is not decoded. Use this function to read the decoded |
| 82 | * value of such a text span in an HTML document. |
| 83 | * |
| 84 | * Example: |
| 85 | * |
| 86 | * '“😄”' === WP_HTML_Decode::decode_text_node( '“😄”' ); |
| 87 | * |
| 88 | * @param string $text Text containing raw and non-decoded text node to decode. |
| 89 | * |
| 90 | * @return string Decoded UTF-8 value of given text node. |
| 91 | * @since 6.6.0 |
| 92 | */ |
| 93 | public static function decode_text_node( $text ): string { |
| 94 | return static::decode( 'data', $text ); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Returns a string containing the decoded value of a given HTML attribute. |
| 99 | * |
| 100 | * Text found inside an HTML attribute has different parsing rules than for |
| 101 | * text found inside other markup, or DATA segments. Use this function to |
| 102 | * read the decoded value of an HTML string inside a quoted attribute. |
| 103 | * |
| 104 | * Example: |
| 105 | * |
| 106 | * '“😄”' === WP_HTML_Decode::decode_attribute( '“😄”' ); |
| 107 | * |
| 108 | * @param string $text Text containing raw and non-decoded attribute value to decode. |
| 109 | * |
| 110 | * @return string Decoded UTF-8 value of given attribute value. |
| 111 | * @since 6.6.0 |
| 112 | */ |
| 113 | public static function decode_attribute( $text ): string { |
| 114 | return static::decode( 'attribute', $text ); |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Decodes a span of HTML text, depending on the context in which it's found. |
| 119 | * |
| 120 | * This is a low-level method; prefer calling WP_HTML_Decoder::decode_attribute() or |
| 121 | * WP_HTML_Decoder::decode_text_node() instead. It's provided for cases where this |
| 122 | * may be difficult to do from calling code. |
| 123 | * |
| 124 | * Example: |
| 125 | * |
| 126 | * '©' = WP_HTML_Decoder::decode( 'data', '©' ); |
| 127 | * |
| 128 | * @param string $context `attribute` for decoding attribute values, `data` otherwise. |
| 129 | * @param string $text Text document containing span of text to decode. |
| 130 | * |
| 131 | * @return string Decoded UTF-8 string. |
| 132 | * @since 6.6.0 |
| 133 | * |
| 134 | * @access private |
| 135 | */ |
| 136 | public static function decode( $context, $text ): string { |
| 137 | $decoded = ''; |
| 138 | $end = strlen( $text ); |
| 139 | $at = 0; |
| 140 | $was_at = 0; |
| 141 | |
| 142 | while ( $at < $end ) { |
| 143 | $next_character_reference_at = strpos( $text, '&', $at ); |
| 144 | if ( false === $next_character_reference_at ) { |
| 145 | break; |
| 146 | } |
| 147 | |
| 148 | $character_reference = self::read_character_reference( $context, $text, $next_character_reference_at, $token_length ); |
| 149 | if ( isset( $character_reference ) ) { |
| 150 | $at = $next_character_reference_at; |
| 151 | $decoded .= substr( $text, $was_at, $at - $was_at ); |
| 152 | $decoded .= $character_reference; |
| 153 | $at += $token_length; |
| 154 | $was_at = $at; |
| 155 | continue; |
| 156 | } |
| 157 | |
| 158 | ++$at; |
| 159 | } |
| 160 | |
| 161 | if ( 0 === $was_at ) { |
| 162 | return $text; |
| 163 | } |
| 164 | |
| 165 | if ( $was_at < $end ) { |
| 166 | $decoded .= substr( $text, $was_at, $end - $was_at ); |
| 167 | } |
| 168 | |
| 169 | return $decoded; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Attempt to read a character reference at the given location in a given string, |
| 174 | * depending on the context in which it's found. |
| 175 | * |
| 176 | * If a character reference is found, this function will return the translated value |
| 177 | * that the reference maps to. It will then set `$match_byte_length` the |
| 178 | * number of bytes of input it read while consuming the character reference. This |
| 179 | * gives calling code the opportunity to advance its cursor when traversing a string |
| 180 | * and decoding. |
| 181 | * |
| 182 | * Example: |
| 183 | * |
| 184 | * null === WP_HTML_Decoder::read_character_reference( 'attribute', 'Ships…', 0 ); |
| 185 | * '…' === WP_HTML_Decoder::read_character_reference( 'attribute', 'Ships…', 5, $token_length ); |
| 186 | * 8 === $token_length; // `…` |
| 187 | * |
| 188 | * null === WP_HTML_Decoder::read_character_reference( 'attribute', '¬in', 0 ); |
| 189 | * '∉' === WP_HTML_Decoder::read_character_reference( 'attribute', '∉', 0, $token_length ); |
| 190 | * 7 === $token_length; // `∉` |
| 191 | * |
| 192 | * '¬' === WP_HTML_Decoder::read_character_reference( 'data', '¬in', 0, $token_length ); |
| 193 | * 4 === $token_length; // `¬` |
| 194 | * '∉' === WP_HTML_Decoder::read_character_reference( 'data', '∉', 0, $token_length ); |
| 195 | * 7 === $token_length; // `∉` |
| 196 | * |
| 197 | * @param string $context `attribute` for decoding attribute values, `data` otherwise. |
| 198 | * @param string $text Text document containing span of text to decode. |
| 199 | * @param int $at Optional. Byte offset into text where span begins, defaults to the beginning (0). |
| 200 | * @param int &$match_byte_length Optional. Set to byte-length of character reference if provided and if a match |
| 201 | * is found, otherwise not set. Default null. |
| 202 | * |
| 203 | * @return string|false Decoded character reference in UTF-8 if found, otherwise `false`. |
| 204 | * @global WP_Token_Map $html5_named_character_references Mappings for HTML5 named character references. |
| 205 | * |
| 206 | * @since 6.6.0 |
| 207 | */ |
| 208 | public static function read_character_reference( $context, $text, $at = 0, &$match_byte_length = null ) { |
| 209 | /** |
| 210 | * Mappings for HTML5 named character references. |
| 211 | * |
| 212 | * Loaded lazily on first call to avoid declaring WP_Token_Map at |
| 213 | * Composer bootstrap. Eager loading via autoload.files breaks |
| 214 | * consumers that also load WordPress core, since both shipments |
| 215 | * declare the same WP_* classes and PHP fatals on the second |
| 216 | * declaration. WordPress sets this global itself, so in a WP |
| 217 | * context we never include our copy. |
| 218 | * |
| 219 | * @var WP_Token_Map $html5_named_character_references |
| 220 | */ |
| 221 | global $html5_named_character_references; |
| 222 | |
| 223 | if ( ! isset( $html5_named_character_references ) ) { |
| 224 | require_once __DIR__ . '/html5-named-character-references.php'; |
| 225 | } |
| 226 | |
| 227 | $length = strlen( $text ); |
| 228 | if ( $at + 1 >= $length ) { |
| 229 | return null; |
| 230 | } |
| 231 | |
| 232 | if ( '&' !== $text[ $at ] ) { |
| 233 | return null; |
| 234 | } |
| 235 | |
| 236 | /* |
| 237 | * Numeric character references. |
| 238 | * |
| 239 | * When truncated, these will encode the code point found by parsing the |
| 240 | * digits that are available. For example, when `🅰` is truncated |
| 241 | * to `DZ` it will encode `DZ`. It does not: |
| 242 | * - know how to parse the original `� |
| 243 | �`. |
| 244 | * - fail to parse and return plaintext `DZ`. |
| 245 | * - fail to parse and return the replacement character `�` |
| 246 | */ |
| 247 | if ( '#' === $text[ $at + 1 ] ) { |
| 248 | if ( $at + 2 >= $length ) { |
| 249 | return null; |
| 250 | } |
| 251 | |
| 252 | /** Tracks inner parsing within the numeric character reference. */ |
| 253 | $digits_at = $at + 2; |
| 254 | |
| 255 | if ( 'x' === $text[ $digits_at ] || 'X' === $text[ $digits_at ] ) { |
| 256 | $numeric_base = 16; |
| 257 | $numeric_digits = '0123456789abcdefABCDEF'; |
| 258 | $max_digits = 6; // . |
| 259 | ++$digits_at; |
| 260 | } else { |
| 261 | $numeric_base = 10; |
| 262 | $numeric_digits = '0123456789'; |
| 263 | $max_digits = 7; // . |
| 264 | } |
| 265 | |
| 266 | // Cannot encode invalid Unicode code points. Max is to U+10FFFF. |
| 267 | $zero_count = strspn( $text, '0', $digits_at ); |
| 268 | $digit_count = strspn( $text, $numeric_digits, $digits_at + $zero_count ); |
| 269 | $after_digits = $digits_at + $zero_count + $digit_count; |
| 270 | $has_semicolon = $after_digits < $length && ';' === $text[ $after_digits ]; |
| 271 | $end_of_span = $has_semicolon ? $after_digits + 1 : $after_digits; |
| 272 | |
| 273 | // `&#` or `&#x` without digits returns into plaintext. |
| 274 | if ( 0 === $digit_count && 0 === $zero_count ) { |
| 275 | return null; |
| 276 | } |
| 277 | |
| 278 | // Whereas `&#` and only zeros is invalid. |
| 279 | if ( 0 === $digit_count ) { |
| 280 | $match_byte_length = $end_of_span - $at; |
| 281 | |
| 282 | return '�'; |
| 283 | } |
| 284 | |
| 285 | // If there are too many digits then it's not worth parsing. It's invalid. |
| 286 | if ( $digit_count > $max_digits ) { |
| 287 | $match_byte_length = $end_of_span - $at; |
| 288 | |
| 289 | return '�'; |
| 290 | } |
| 291 | |
| 292 | $digits = substr( $text, $digits_at + $zero_count, $digit_count ); |
| 293 | $codepoint = intval( $digits, $numeric_base ); |
| 294 | |
| 295 | /* |
| 296 | * Noncharacters, 0x0D, and non-ASCII-whitespace control characters. |
| 297 | * |
| 298 | * > A noncharacter is a code point that is in the range U+FDD0 to U+FDEF, |
| 299 | * > inclusive, or U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, |
| 300 | * > U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, |
| 301 | * > U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, |
| 302 | * > U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE, |
| 303 | * > U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, or U+10FFFF. |
| 304 | * |
| 305 | * A C0 control is a code point that is in the range of U+00 to U+1F, |
| 306 | * but ASCII whitespace includes U+09, U+0A, U+0C, and U+0D. |
| 307 | * |
| 308 | * These characters are invalid but still decode as any valid character. |
| 309 | * This comment is here to note and explain why there's no check to |
| 310 | * remove these characters or replace them. |
| 311 | * |
| 312 | * @see https://infra.spec.whatwg.org/#noncharacter |
| 313 | */ |
| 314 | |
| 315 | /* |
| 316 | * Code points in the C1 controls area need to be remapped as if they |
| 317 | * were stored in Windows-1252. Note! This transformation only happens |
| 318 | * for numeric character references. The raw code points in the byte |
| 319 | * stream are not translated. |
| 320 | * |
| 321 | * > If the number is one of the numbers in the first column of |
| 322 | * > the following table, then find the row with that number in |
| 323 | * > the first column, and set the character reference code to |
| 324 | * > the number in the second column of that row. |
| 325 | */ |
| 326 | if ( $codepoint >= 0x80 && $codepoint <= 0x9F ) { |
| 327 | $windows_1252_mapping = array( |
| 328 | 0x20AC, // 0x80 -> EURO SIGN (€). |
| 329 | 0x81, // 0x81 -> (no change). |
| 330 | 0x201A, // 0x82 -> SINGLE LOW-9 QUOTATION MARK (‚). |
| 331 | 0x0192, // 0x83 -> LATIN SMALL LETTER F WITH HOOK (ƒ). |
| 332 | 0x201E, // 0x84 -> DOUBLE LOW-9 QUOTATION MARK („). |
| 333 | 0x2026, // 0x85 -> HORIZONTAL ELLIPSIS (…). |
| 334 | 0x2020, // 0x86 -> DAGGER (†). |
| 335 | 0x2021, // 0x87 -> DOUBLE DAGGER (‡). |
| 336 | 0x02C6, // 0x88 -> MODIFIER LETTER CIRCUMFLEX ACCENT (ˆ). |
| 337 | 0x2030, // 0x89 -> PER MILLE SIGN (‰). |
| 338 | 0x0160, // 0x8A -> LATIN CAPITAL LETTER S WITH CARON (Š). |
| 339 | 0x2039, // 0x8B -> SINGLE LEFT-POINTING ANGLE QUOTATION MARK (‹). |
| 340 | 0x0152, // 0x8C -> LATIN CAPITAL LIGATURE OE (Œ). |
| 341 | 0x8D, // 0x8D -> (no change). |
| 342 | 0x017D, // 0x8E -> LATIN CAPITAL LETTER Z WITH CARON (Ž). |
| 343 | 0x8F, // 0x8F -> (no change). |
| 344 | 0x90, // 0x90 -> (no change). |
| 345 | 0x2018, // 0x91 -> LEFT SINGLE QUOTATION MARK (‘). |
| 346 | 0x2019, // 0x92 -> RIGHT SINGLE QUOTATION MARK (’). |
| 347 | 0x201C, // 0x93 -> LEFT DOUBLE QUOTATION MARK (“). |
| 348 | 0x201D, // 0x94 -> RIGHT DOUBLE QUOTATION MARK (”). |
| 349 | 0x2022, // 0x95 -> BULLET (•). |
| 350 | 0x2013, // 0x96 -> EN DASH (–). |
| 351 | 0x2014, // 0x97 -> EM DASH (—). |
| 352 | 0x02DC, // 0x98 -> SMALL TILDE (˜). |
| 353 | 0x2122, // 0x99 -> TRADE MARK SIGN (™). |
| 354 | 0x0161, // 0x9A -> LATIN SMALL LETTER S WITH CARON (š). |
| 355 | 0x203A, // 0x9B -> SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (›). |
| 356 | 0x0153, // 0x9C -> LATIN SMALL LIGATURE OE (œ). |
| 357 | 0x9D, // 0x9D -> (no change). |
| 358 | 0x017E, // 0x9E -> LATIN SMALL LETTER Z WITH CARON (ž). |
| 359 | 0x0178, // 0x9F -> LATIN CAPITAL LETTER Y WITH DIAERESIS (Ÿ). |
| 360 | ); |
| 361 | |
| 362 | $codepoint = $windows_1252_mapping[ $codepoint - 0x80 ]; |
| 363 | } |
| 364 | |
| 365 | $match_byte_length = $end_of_span - $at; |
| 366 | |
| 367 | return self::codepoint_to_utf8_bytes( $codepoint ); |
| 368 | } |
| 369 | |
| 370 | /** Tracks inner parsing within the named character reference. */ |
| 371 | $name_at = $at + 1; |
| 372 | // Minimum named character reference is two characters. E.g. `GT`. |
| 373 | if ( $name_at + 2 > $length ) { |
| 374 | return null; |
| 375 | } |
| 376 | |
| 377 | $name_length = 0; |
| 378 | $replacement = $html5_named_character_references->read_token( $text, $name_at, $name_length ); |
| 379 | if ( false === $replacement ) { |
| 380 | return null; |
| 381 | } |
| 382 | |
| 383 | $after_name = $name_at + $name_length; |
| 384 | |
| 385 | // If the match ended with a semicolon then it should always be decoded. |
| 386 | if ( ';' === $text[ $name_at + $name_length - 1 ] ) { |
| 387 | $match_byte_length = $after_name - $at; |
| 388 | |
| 389 | return $replacement; |
| 390 | } |
| 391 | |
| 392 | /* |
| 393 | * At this point though there's a match for an entry in the named |
| 394 | * character reference table but the match doesn't end in `;`. |
| 395 | * It may be allowed if it's followed by something unambiguous. |
| 396 | */ |
| 397 | $ambiguous_follower = ( |
| 398 | $after_name < $length && |
| 399 | $name_at < $length && |
| 400 | ( |
| 401 | ctype_alnum( $text[ $after_name ] ) || |
| 402 | '=' === $text[ $after_name ] |
| 403 | ) |
| 404 | ); |
| 405 | |
| 406 | // It's non-ambiguous, safe to leave it in. |
| 407 | if ( ! $ambiguous_follower ) { |
| 408 | $match_byte_length = $after_name - $at; |
| 409 | |
| 410 | return $replacement; |
| 411 | } |
| 412 | |
| 413 | // It's ambiguous, which isn't allowed inside attributes. |
| 414 | if ( 'attribute' === $context ) { |
| 415 | return null; |
| 416 | } |
| 417 | |
| 418 | $match_byte_length = $after_name - $at; |
| 419 | |
| 420 | return $replacement; |
| 421 | } |
| 422 | |
| 423 | /** |
| 424 | * Encode a code point number into the UTF-8 encoding. |
| 425 | * |
| 426 | * This encoder implements the UTF-8 encoding algorithm for converting |
| 427 | * a code point into a byte sequence. If it receives an invalid code |
| 428 | * point it will return the Unicode Replacement Character U+FFFD `�`. |
| 429 | * |
| 430 | * Example: |
| 431 | * |
| 432 | * '� |
| 433 | �' === WP_HTML_Decoder::codepoint_to_utf8_bytes( 0x1f170 ); |
| 434 | * |
| 435 | * // Half of a surrogate pair is an invalid code point. |
| 436 | * '�' === WP_HTML_Decoder::codepoint_to_utf8_bytes( 0xd83c ); |
| 437 | * |
| 438 | * @param int $codepoint Which code point to convert. |
| 439 | * |
| 440 | * @return string Converted code point, or `�` if invalid. |
| 441 | * @since 6.6.0 |
| 442 | * |
| 443 | * @see https://www.rfc-editor.org/rfc/rfc3629 For the UTF-8 standard. |
| 444 | */ |
| 445 | public static function codepoint_to_utf8_bytes( $codepoint ): string { |
| 446 | // Pre-check to ensure a valid code point. |
| 447 | if ( |
| 448 | $codepoint <= 0 || |
| 449 | ( $codepoint >= 0xD800 && $codepoint <= 0xDFFF ) || |
| 450 | $codepoint > 0x10FFFF |
| 451 | ) { |
| 452 | return '�'; |
| 453 | } |
| 454 | |
| 455 | if ( $codepoint <= 0x7F ) { |
| 456 | return chr( $codepoint ); |
| 457 | } |
| 458 | |
| 459 | if ( $codepoint <= 0x7FF ) { |
| 460 | $byte1 = chr( ( $codepoint >> 6 ) | 0xC0 ); |
| 461 | $byte2 = chr( $codepoint & 0x3F | 0x80 ); |
| 462 | |
| 463 | return "{$byte1}{$byte2}"; |
| 464 | } |
| 465 | |
| 466 | if ( $codepoint <= 0xFFFF ) { |
| 467 | $byte1 = chr( ( $codepoint >> 12 ) | 0xE0 ); |
| 468 | $byte2 = chr( ( $codepoint >> 6 ) & 0x3F | 0x80 ); |
| 469 | $byte3 = chr( $codepoint & 0x3F | 0x80 ); |
| 470 | |
| 471 | return "{$byte1}{$byte2}{$byte3}"; |
| 472 | } |
| 473 | |
| 474 | // Any values above U+10FFFF are eliminated above in the pre-check. |
| 475 | $byte1 = chr( ( $codepoint >> 18 ) | 0xF0 ); |
| 476 | $byte2 = chr( ( $codepoint >> 12 ) & 0x3F | 0x80 ); |
| 477 | $byte3 = chr( ( $codepoint >> 6 ) & 0x3F | 0x80 ); |
| 478 | $byte4 = chr( $codepoint & 0x3F | 0x80 ); |
| 479 | |
| 480 | return "{$byte1}{$byte2}{$byte3}{$byte4}"; |
| 481 | } |
| 482 | } |
| 483 |