From 6fc419a82a55c1fb2c3d49686b298d6f8622245e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 9 Jun 2026 17:45:11 +0200 Subject: [PATCH 1/2] Add Symfony Package Radar source files --- blueprints/symfony-package-radar/README.md | 26 + .../source/wordpress/index.php | 13 + .../wordpress-html-api/LICENSE.md | 384 + .../wordpress-html-api/README.md | 8 + .../wordpress-html-api/composer.json | 26 + .../src/class-wp-token-map.php | 820 ++ ...ass-wp-html-active-formatting-elements.php | 229 + .../class-wp-html-attribute-token.php | 116 + .../src/html-api/class-wp-html-decoder.php | 463 ++ .../html-api/class-wp-html-doctype-info.php | 616 ++ .../html-api/class-wp-html-open-elements.php | 852 +++ .../class-wp-html-processor-state.php | 454 ++ .../src/html-api/class-wp-html-processor.php | 6622 +++++++++++++++++ .../src/html-api/class-wp-html-span.php | 56 + .../html-api/class-wp-html-stack-event.php | 82 + .../html-api/class-wp-html-tag-processor.php | 4576 ++++++++++++ .../class-wp-html-text-replacement.php | 64 + .../src/html-api/class-wp-html-token.php | 126 + .../class-wp-html-unsupported-exception.php | 115 + .../html5-named-character-references.php | 1313 ++++ .../symfony-package-radar/composer.json | 28 + .../symfony-package-radar/composer.lock | 2593 +++++++ .../symfony-package-radar/public/index.php | 12 + .../src/Controller/DashboardController.php | 36 + .../symfony-package-radar/src/Kernel.php | 58 + .../src/Service/SymfonyPackageRadar.php | 89 + .../templates/dashboard.html.twig | 59 + 27 files changed, 19836 insertions(+) create mode 100644 blueprints/symfony-package-radar/README.md create mode 100644 blueprints/symfony-package-radar/source/wordpress/index.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/LICENSE.md create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/README.md create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/composer.json create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/class-wp-token-map.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-active-formatting-elements.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-attribute-token.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-decoder.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-doctype-info.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-open-elements.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-processor-state.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-processor.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-span.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-stack-event.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-tag-processor.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-text-replacement.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-token.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-unsupported-exception.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/html5-named-character-references.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer.json create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer.lock create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/public/index.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/src/Controller/DashboardController.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/src/Kernel.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/src/Service/SymfonyPackageRadar.php create mode 100644 blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/templates/dashboard.html.twig diff --git a/blueprints/symfony-package-radar/README.md b/blueprints/symfony-package-radar/README.md new file mode 100644 index 00000000..e5cf4d20 --- /dev/null +++ b/blueprints/symfony-package-radar/README.md @@ -0,0 +1,26 @@ +# Symfony Package Radar Blueprint + +This directory contains a PHP-only Symfony demo for WordPress Playground. + +- `blueprint.json` loads the demo without downloading WordPress (`preferredVersions.wp: false`). +- `symfony-package-radar.zip` is the bundled app used by the Blueprint. +- `source/wordpress/` contains the reviewable source used to build the ZIP. + +To rebuild the ZIP: + +```bash +rm -rf .context/symfony-package-radar-build +mkdir -p .context/symfony-package-radar-build +cp -R blueprints/symfony-package-radar/source/wordpress/. \ + .context/symfony-package-radar-build/ +( + cd .context/symfony-package-radar-build/symfony-package-radar + composer install --no-dev --no-interaction --no-progress --prefer-dist --optimize-autoloader +) +( + cd .context/symfony-package-radar-build + zip -X -qr ../../blueprints/symfony-package-radar/symfony-package-radar.zip \ + index.php symfony-package-radar \ + -x 'symfony-package-radar/var/cache/*' 'symfony-package-radar/var/log/*' +) +``` diff --git a/blueprints/symfony-package-radar/source/wordpress/index.php b/blueprints/symfony-package-radar/source/wordpress/index.php new file mode 100644 index 00000000..1a1ed981 --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/index.php @@ -0,0 +1,13 @@ + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + +WRITTEN OFFER + +The source code for any program binaries or compressed scripts that are +included with WordPress can be freely obtained at the following URL: + + https://wordpress.org/download/source/ diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/README.md b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/README.md new file mode 100644 index 00000000..cbe17fe1 --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/README.md @@ -0,0 +1,8 @@ +# WordPress HTML API for the Symfony Playground demo + +This local Composer package bundles the WordPress HTML API runtime files used by +the Symfony Playground documentation example. It provides `WP_HTML_Processor` +without bundling, installing, or booting WordPress itself. + +The PHP files were copied from the WordPress core `wp-includes/html-api/` package +layout and are licensed under GPL-2.0-or-later. diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/composer.json b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/composer.json new file mode 100644 index 00000000..1723b627 --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/composer.json @@ -0,0 +1,26 @@ +{ + "name": "playground/wordpress-html-api", + "description": "WordPress HTML API classes bundled for the Symfony Playground demo.", + "type": "library", + "license": "GPL-2.0-or-later", + "autoload": { + "files": [ + "src/class-wp-token-map.php", + "src/html-api/class-wp-html-attribute-token.php", + "src/html-api/class-wp-html-span.php", + "src/html-api/class-wp-html-text-replacement.php", + "src/html-api/class-wp-html-token.php", + "src/html-api/class-wp-html-unsupported-exception.php", + "src/html-api/class-wp-html-active-formatting-elements.php", + "src/html-api/class-wp-html-open-elements.php", + "src/html-api/class-wp-html-processor-state.php", + "src/html-api/class-wp-html-stack-event.php", + "src/html-api/class-wp-html-doctype-info.php", + "src/html-api/html5-named-character-references.php", + "src/html-api/class-wp-html-decoder.php", + "src/html-api/class-wp-html-tag-processor.php", + "src/html-api/class-wp-html-processor.php" + ] + }, + "version": "1.0.0" +} diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/class-wp-token-map.php b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/class-wp-token-map.php new file mode 100644 index 00000000..09a0b930 --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/class-wp-token-map.php @@ -0,0 +1,820 @@ + '😯', + * ':(' => '🙁', + * ':)' => '🙂', + * ':?' => '😕', + * ) ); + * + * true === $smilies->contains( ':)' ); + * false === $smilies->contains( 'simile' ); + * + * '😕' === $smilies->read_token( 'Not sure :?.', 9, $length_of_smily_syntax ); + * 2 === $length_of_smily_syntax; + * + * ## Precomputing the Token Map. + * + * Creating the class involves some work sorting and organizing the tokens and their + * replacement values. In order to skip this, it's possible for the class to export + * its state and be used as actual PHP source code. + * + * Example: + * + * // Export with four spaces as the indent, only for the sake of this docblock. + * // The default indent is a tab character. + * $indent = ' '; + * echo $smilies->precomputed_php_source_table( $indent ); + * + * // Output, to be pasted into a PHP source file: + * WP_Token_Map::from_precomputed_table( + * array( + * "storage_version" => "6.6.0", + * "key_length" => 2, + * "groups" => "", + * "long_words" => array(), + * "small_words" => "8O\x00:)\x00:(\x00:?\x00", + * "small_mappings" => array( "😯", "🙂", "🙁", "😕" ) + * ) + * ); + * + * ## Large vs. small words. + * + * This class uses a short prefix called the "key" to optimize lookup of its tokens. + * This means that some tokens may be shorter than or equal in length to that key. + * Those words that are longer than the key are called "large" while those shorter + * than or equal to the key length are called "small." + * + * This separation of large and small words is incidental to the way this class + * optimizes lookup, and should be considered an internal implementation detail + * of the class. It may still be important to be aware of it, however. + * + * ## Determining Key Length. + * + * The choice of the size of the key length should be based on the data being stored in + * the token map. It should divide the data as evenly as possible, but should not create + * so many groups that a large fraction of the groups only contain a single token. + * + * For the HTML5 named character references, a key length of 2 was found to provide a + * sufficient spread and should be a good default for relatively large sets of tokens. + * + * However, for some data sets this might be too long. For example, a list of smilies + * may be too small for a key length of 2. Perhaps 1 would be more appropriate. It's + * best to experiment and determine empirically which values are appropriate. + * + * ## Generate Pre-Computed Source Code. + * + * Since the `WP_Token_Map` is designed for relatively static lookups, it can be + * advantageous to precompute the values and instantiate a table that has already + * sorted and grouped the tokens and built the lookup strings. + * + * This can be done with `WP_Token_Map::precomputed_php_source_table()`. + * + * Note that if there is a leading character that all tokens need, such as `&` for + * HTML named character references, it can be beneficial to exclude this from the + * token map. Instead, find occurrences of the leading character and then use the + * token map to see if the following characters complete the token. + * + * Example: + * + * $map = WP_Token_Map::from_array( array( 'simple_smile:' => '🙂', 'sob:' => '😭', 'soba:' => '🍜' ) ); + * echo $map->precomputed_php_source_table(); + * // Output + * WP_Token_Map::from_precomputed_table( + * array( + * "storage_version" => "6.6.0", + * "key_length" => 2, + * "groups" => "si\x00so\x00", + * "long_words" => array( + * // simple_smile:[🙂]. + * "\x0bmple_smile:\x04🙂", + * // soba:[🍜] sob:[😭]. + * "\x03ba:\x04🍜\x02b:\x04😭", + * ), + * "short_words" => "", + * "short_mappings" => array() + * } + * ); + * + * This precomputed value can be stored directly in source code and will skip the + * startup cost of generating the lookup strings. See `$html5_named_character_entities`. + * + * Note that any updates to the precomputed format should update the storage version + * constant. It would also be best to provide an update function to take older known + * versions and upgrade them in place when loading into `from_precomputed_table()`. + * + * ## Future Direction. + * + * It may be viable to dynamically increase the length limits such that there's no need to impose them. + * The limit appears because of the packing structure, which indicates how many bytes each segment of + * text in the lookup tables spans. If, however, care were taken to track the longest word length, then + * the packing structure could change its representation to allow for that. Each additional byte storing + * length, however, increases the memory overhead and lookup runtime. + * + * An alternative approach could be to borrow the UTF-8 variable-length encoding and store lengths of less + * than 127 as a single byte with the high bit unset, storing longer lengths as the combination of + * continuation bytes. + * + * Since it has not been shown during the development of this class that longer strings are required, this + * update is deferred until such a need is clear. + * + * @since 6.6.0 + */ +class WP_Token_Map { + /** + * Denotes the version of the code which produces pre-computed source tables. + * + * This version will be used not only to verify pre-computed data, but also + * to upgrade pre-computed data from older versions. Choosing a name that + * corresponds to the WordPress release will help people identify where an + * old copy of data came from. + */ + const STORAGE_VERSION = '6.6.0-trunk'; + + /** + * Maximum length for each key and each transformed value in the table (in bytes). + * + * @since 6.6.0 + */ + const MAX_LENGTH = 256; + + /** + * How many bytes of each key are used to form a group key for lookup. + * This also determines whether a word is considered short or long. + * + * @since 6.6.0 + * + * @var int + */ + private $key_length = 2; + + /** + * Stores an optimized form of the word set, where words are grouped + * by a prefix of the `$key_length` and then collapsed into a string. + * + * In each group, the keys and lookups form a packed data structure. + * The keys in the string are stripped of their "group key," which is + * the prefix of length `$this->key_length` shared by all of the items + * in the group. Each word in the string is prefixed by a single byte + * whose raw unsigned integer value represents how many bytes follow. + * + * ┌────────────────┬───────────────┬─────────────────┬────────┐ + * │ Length of rest │ Rest of key │ Length of value │ Value │ + * │ of key (bytes) │ │ (bytes) │ │ + * ├────────────────┼───────────────┼─────────────────┼────────┤ + * │ 0x08 │ nterDot; │ 0x02 │ · │ + * └────────────────┴───────────────┴─────────────────┴────────┘ + * + * In this example, the key `CenterDot;` has a group key `Ce`, leaving + * eight bytes for the rest of the key, `nterDot;`, and two bytes for + * the transformed value `·` (or U+B7 or "\xC2\xB7"). + * + * Example: + * + * // Stores array( 'CenterDot;' => '·', 'Cedilla;' => '¸' ). + * $groups = "Ce\x00"; + * $large_words = array( "\x08nterDot;\x02·\x06dilla;\x02¸" ) + * + * The prefixes appear in the `$groups` string, each followed by a null + * byte. This makes for quick lookup of where in the group string the key + * is found, and then a simple division converts that offset into the index + * in the `$large_words` array where the group string is to be found. + * + * This lookup data structure is designed to optimize cache locality and + * minimize indirect memory reads when matching strings in the set. + * + * @since 6.6.0 + * + * @var array + */ + private $large_words = array(); + + /** + * Stores the group keys for sequential string lookup. + * + * The offset into this string where the group key appears corresponds with the index + * into the group array where the rest of the group string appears. This is an optimization + * to improve cache locality while searching and minimize indirect memory accesses. + * + * @since 6.6.0 + * + * @var string + */ + private $groups = ''; + + /** + * Stores an optimized row of small words, where every entry is + * `$this->key_size + 1` bytes long and zero-extended. + * + * This packing allows for direct lookup of a short word followed + * by the null byte, if extended to `$this->key_size + 1`. + * + * Example: + * + * // Stores array( 'GT', 'LT', 'gt', 'lt' ). + * "GT\x00LT\x00gt\x00lt\x00" + * + * @since 6.6.0 + * + * @var string + */ + private $small_words = ''; + + /** + * Replacements for the small words, in the same order they appear. + * + * With the position of a small word it's possible to index the translation + * directly, as its position in the `$small_words` string corresponds to + * the index of the replacement in the `$small_mapping` array. + * + * Example: + * + * array( '>', '<', '>', '<' ) + * + * @since 6.6.0 + * + * @var string[] + */ + private $small_mappings = array(); + + /** + * Create a token map using an associative array of key/value pairs as the input. + * + * Example: + * + * $smilies = WP_Token_Map::from_array( array( + * '8O' => '😯', + * ':(' => '🙁', + * ':)' => '🙂', + * ':?' => '😕', + * ) ); + * + * @since 6.6.0 + * + * @param array $mappings The keys transform into the values, both are strings. + * @param int $key_length Determines the group key length. Leave at the default value + * of 2 unless there's an empirical reason to change it. + * + * @return WP_Token_Map|null Token map, unless unable to create it. + */ + public static function from_array( array $mappings, int $key_length = 2 ): ?WP_Token_Map { + $map = new WP_Token_Map(); + $map->key_length = $key_length; + + // Start by grouping words. + + $groups = array(); + $shorts = array(); + foreach ( $mappings as $word => $mapping ) { + if ( + self::MAX_LENGTH <= strlen( $word ) || + self::MAX_LENGTH <= strlen( $mapping ) + ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: maximum byte length (a count) */ + __( 'Token Map tokens and substitutions must all be shorter than %1$d bytes.' ), + self::MAX_LENGTH + ), + '6.6.0' + ); + return null; + } + + $length = strlen( $word ); + + if ( $key_length >= $length ) { + $shorts[] = $word; + } else { + $group = substr( $word, 0, $key_length ); + + if ( ! isset( $groups[ $group ] ) ) { + $groups[ $group ] = array(); + } + + $groups[ $group ][] = array( substr( $word, $key_length ), $mapping ); + } + } + + /* + * Sort the words to ensure that no smaller substring of a match masks the full match. + * For example, `Cap` should not match before `CapitalDifferentialD`. + */ + usort( $shorts, 'WP_Token_Map::longest_first_then_alphabetical' ); + foreach ( $groups as $group_key => $group ) { + usort( + $groups[ $group_key ], + static function ( array $a, array $b ): int { + return self::longest_first_then_alphabetical( $a[0], $b[0] ); + } + ); + } + + // Finally construct the optimized lookups. + + foreach ( $shorts as $word ) { + $map->small_words .= str_pad( $word, $key_length + 1, "\x00", STR_PAD_RIGHT ); + $map->small_mappings[] = $mappings[ $word ]; + } + + $group_keys = array_keys( $groups ); + sort( $group_keys ); + + foreach ( $group_keys as $group ) { + $map->groups .= "{$group}\x00"; + + $group_string = ''; + + foreach ( $groups[ $group ] as $group_word ) { + list( $word, $mapping ) = $group_word; + + $word_length = pack( 'C', strlen( $word ) ); + $mapping_length = pack( 'C', strlen( $mapping ) ); + $group_string .= "{$word_length}{$word}{$mapping_length}{$mapping}"; + } + + $map->large_words[] = $group_string; + } + + return $map; + } + + /** + * Creates a token map from a pre-computed table. + * This skips the initialization cost of generating the table. + * + * This function should only be used to load data created with + * WP_Token_Map::precomputed_php_source_tag(). + * + * @since 6.6.0 + * + * @param array $state { + * Stores pre-computed state for directly loading into a Token Map. + * + * @type string $storage_version Which version of the code produced this state. + * @type int $key_length Group key length. + * @type string $groups Group lookup index. + * @type array $large_words Large word groups and packed strings. + * @type string $small_words Small words packed string. + * @type array $small_mappings Small word mappings. + * } + * + * @return WP_Token_Map Map with precomputed data loaded. + */ + public static function from_precomputed_table( $state ): ?WP_Token_Map { + $has_necessary_state = isset( + $state['storage_version'], + $state['key_length'], + $state['groups'], + $state['large_words'], + $state['small_words'], + $state['small_mappings'] + ); + + if ( ! $has_necessary_state ) { + _doing_it_wrong( + __METHOD__, + __( 'Missing required inputs to pre-computed WP_Token_Map.' ), + '6.6.0' + ); + return null; + } + + if ( self::STORAGE_VERSION !== $state['storage_version'] ) { + _doing_it_wrong( + __METHOD__, + /* translators: 1: version string, 2: version string. */ + sprintf( __( 'Loaded version \'%1$s\' incompatible with expected version \'%2$s\'.' ), $state['storage_version'], self::STORAGE_VERSION ), + '6.6.0' + ); + return null; + } + + $map = new WP_Token_Map(); + + $map->key_length = $state['key_length']; + $map->groups = $state['groups']; + $map->large_words = $state['large_words']; + $map->small_words = $state['small_words']; + $map->small_mappings = $state['small_mappings']; + + return $map; + } + + /** + * Indicates if a given word is a lookup key in the map. + * + * Example: + * + * true === $smilies->contains( ':)' ); + * false === $smilies->contains( 'simile' ); + * + * @since 6.6.0 + * + * @param string $word Determine if this word is a lookup key in the map. + * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching. Default 'case-sensitive'. + * @return bool Whether there's an entry for the given word in the map. + */ + public function contains( string $word, string $case_sensitivity = 'case-sensitive' ): bool { + $ignore_case = 'ascii-case-insensitive' === $case_sensitivity; + + if ( $this->key_length >= strlen( $word ) ) { + if ( 0 === strlen( $this->small_words ) ) { + return false; + } + + $term = str_pad( $word, $this->key_length + 1, "\x00", STR_PAD_RIGHT ); + $word_at = $ignore_case ? stripos( $this->small_words, $term ) : strpos( $this->small_words, $term ); + if ( false === $word_at ) { + return false; + } + + return true; + } + + $group_key = substr( $word, 0, $this->key_length ); + $group_at = $ignore_case ? stripos( $this->groups, $group_key ) : strpos( $this->groups, $group_key ); + if ( false === $group_at ) { + return false; + } + $group = $this->large_words[ $group_at / ( $this->key_length + 1 ) ]; + $group_length = strlen( $group ); + $slug = substr( $word, $this->key_length ); + $length = strlen( $slug ); + $at = 0; + + while ( $at < $group_length ) { + $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token_at = $at; + $at += $token_length; + $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping_at = $at; + + if ( $token_length === $length && 0 === substr_compare( $group, $slug, $token_at, $token_length, $ignore_case ) ) { + return true; + } + + $at = $mapping_at + $mapping_length; + } + + return false; + } + + /** + * If the text starting at a given offset is a lookup key in the map, + * return the corresponding transformation from the map, else `false`. + * + * This function returns the translated string, but accepts an optional + * parameter `$matched_token_byte_length`, which communicates how many + * bytes long the lookup key was, if it found one. This can be used to + * advance a cursor in calling code if a lookup key was found. + * + * Example: + * + * false === $smilies->read_token( 'Not sure :?.', 0, $token_byte_length ); + * '😕' === $smilies->read_token( 'Not sure :?.', 9, $token_byte_length ); + * 2 === $token_byte_length; + * + * Example: + * + * while ( $at < strlen( $input ) ) { + * $next_at = strpos( $input, ':', $at ); + * if ( false === $next_at ) { + * break; + * } + * + * $smily = $smilies->read_token( $input, $next_at, $token_byte_length ); + * if ( false === $next_at ) { + * ++$at; + * continue; + * } + * + * $prefix = substr( $input, $at, $next_at - $at ); + * $at += $token_byte_length; + * $output .= "{$prefix}{$smily}"; + * } + * + * @since 6.6.0 + * + * @param string $text String in which to search for a lookup key. + * @param int $offset Optional. How many bytes into the string where the lookup key ought to start. Default 0. + * @param int|null &$matched_token_byte_length Optional. Holds byte-length of found token matched, otherwise not set. Default null. + * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching. Default 'case-sensitive'. + * + * @return string|null Mapped value of lookup key if found, otherwise `null`. + */ + public function read_token( string $text, int $offset = 0, &$matched_token_byte_length = null, $case_sensitivity = 'case-sensitive' ): ?string { + $ignore_case = 'ascii-case-insensitive' === $case_sensitivity; + $text_length = strlen( $text ); + + // Search for a long word first, if the text is long enough, and if that fails, a short one. + if ( $text_length > $this->key_length ) { + $group_key = substr( $text, $offset, $this->key_length ); + + $group_at = $ignore_case ? stripos( $this->groups, $group_key ) : strpos( $this->groups, $group_key ); + if ( false === $group_at ) { + // Perhaps a short word then. + return strlen( $this->small_words ) > 0 + ? $this->read_small_token( $text, $offset, $matched_token_byte_length, $case_sensitivity ) + : null; + } + + $group = $this->large_words[ $group_at / ( $this->key_length + 1 ) ]; + $group_length = strlen( $group ); + $at = 0; + while ( $at < $group_length ) { + $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token = substr( $group, $at, $token_length ); + $at += $token_length; + $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping_at = $at; + + if ( 0 === substr_compare( $text, $token, $offset + $this->key_length, $token_length, $ignore_case ) ) { + $matched_token_byte_length = $this->key_length + $token_length; + return substr( $group, $mapping_at, $mapping_length ); + } + + $at = $mapping_at + $mapping_length; + } + } + + // Perhaps a short word then. + return strlen( $this->small_words ) > 0 + ? $this->read_small_token( $text, $offset, $matched_token_byte_length, $case_sensitivity ) + : null; + } + + /** + * Finds a match for a short word at the index. + * + * @since 6.6.0 + * + * @param string $text String in which to search for a lookup key. + * @param int $offset Optional. How many bytes into the string where the lookup key ought to start. Default 0. + * @param int|null &$matched_token_byte_length Optional. Holds byte-length of found lookup key if matched, otherwise not set. Default null. + * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching. Default 'case-sensitive'. + * + * @return string|null Mapped value of lookup key if found, otherwise `null`. + */ + private function read_small_token( string $text, int $offset = 0, &$matched_token_byte_length = null, $case_sensitivity = 'case-sensitive' ): ?string { + $ignore_case = 'ascii-case-insensitive' === $case_sensitivity; + $small_length = strlen( $this->small_words ); + $search_text = substr( $text, $offset, $this->key_length ); + if ( $ignore_case ) { + $search_text = strtoupper( $search_text ); + } + $starting_char = $search_text[0]; + + $at = 0; + while ( $at < $small_length ) { + if ( + $starting_char !== $this->small_words[ $at ] && + ( ! $ignore_case || strtoupper( $this->small_words[ $at ] ) !== $starting_char ) + ) { + $at += $this->key_length + 1; + continue; + } + + for ( $adjust = 1; $adjust < $this->key_length; $adjust++ ) { + if ( "\x00" === $this->small_words[ $at + $adjust ] ) { + $matched_token_byte_length = $adjust; + return $this->small_mappings[ $at / ( $this->key_length + 1 ) ]; + } + + if ( + $search_text[ $adjust ] !== $this->small_words[ $at + $adjust ] && + ( ! $ignore_case || strtoupper( $this->small_words[ $at + $adjust ] !== $search_text[ $adjust ] ) ) + ) { + $at += $this->key_length + 1; + continue 2; + } + } + + $matched_token_byte_length = $adjust; + return $this->small_mappings[ $at / ( $this->key_length + 1 ) ]; + } + + return null; + } + + /** + * Exports the token map into an associate array of key/value pairs. + * + * Example: + * + * $smilies->to_array() === array( + * '8O' => '😯', + * ':(' => '🙁', + * ':)' => '🙂', + * ':?' => '😕', + * ); + * + * @return array The lookup key/substitution values as an associate array. + */ + public function to_array(): array { + $tokens = array(); + + $at = 0; + $small_mapping = 0; + $small_length = strlen( $this->small_words ); + while ( $at < $small_length ) { + $key = rtrim( substr( $this->small_words, $at, $this->key_length + 1 ), "\x00" ); + $value = $this->small_mappings[ $small_mapping++ ]; + $tokens[ $key ] = $value; + + $at += $this->key_length + 1; + } + + foreach ( $this->large_words as $index => $group ) { + $prefix = substr( $this->groups, $index * ( $this->key_length + 1 ), 2 ); + $group_length = strlen( $group ); + $at = 0; + while ( $at < $group_length ) { + $length = unpack( 'C', $group[ $at++ ] )[1]; + $key = $prefix . substr( $group, $at, $length ); + + $at += $length; + $length = unpack( 'C', $group[ $at++ ] )[1]; + $value = substr( $group, $at, $length ); + + $tokens[ $key ] = $value; + $at += $length; + } + } + + return $tokens; + } + + /** + * Export the token map for quick loading in PHP source code. + * + * This function has a specific purpose, to make loading of static token maps fast. + * It's used to ensure that the HTML character reference lookups add a minimal cost + * to initializing the PHP process. + * + * Example: + * + * echo $smilies->precomputed_php_source_table(); + * + * // Output. + * WP_Token_Map::from_precomputed_table( + * array( + * "storage_version" => "6.6.0", + * "key_length" => 2, + * "groups" => "", + * "long_words" => array(), + * "small_words" => "8O\x00:)\x00:(\x00:?\x00", + * "small_mappings" => array( "😯", "🙂", "🙁", "😕" ) + * ) + * ); + * + * @since 6.6.0 + * + * @param string $indent Optional. Use this string for indentation, or rely on the default horizontal tab character. Default "\t". + * @return string Value which can be pasted into a PHP source file for quick loading of table. + */ + public function precomputed_php_source_table( string $indent = "\t" ): string { + $i1 = $indent; + $i2 = $i1 . $indent; + $i3 = $i2 . $indent; + + $class_version = self::STORAGE_VERSION; + + $output = self::class . "::from_precomputed_table(\n"; + $output .= "{$i1}array(\n"; + $output .= "{$i2}\"storage_version\" => \"{$class_version}\",\n"; + $output .= "{$i2}\"key_length\" => {$this->key_length},\n"; + + $group_line = str_replace( "\x00", "\\x00", $this->groups ); + $output .= "{$i2}\"groups\" => \"{$group_line}\",\n"; + + $output .= "{$i2}\"large_words\" => array(\n"; + + $prefixes = explode( "\x00", $this->groups ); + foreach ( $prefixes as $index => $prefix ) { + if ( '' === $prefix ) { + break; + } + $group = $this->large_words[ $index ]; + $group_length = strlen( $group ); + $comment_line = "{$i3}//"; + $data_line = "{$i3}\""; + $at = 0; + while ( $at < $group_length ) { + $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token = substr( $group, $at, $token_length ); + $at += $token_length; + $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping = substr( $group, $at, $mapping_length ); + $at += $mapping_length; + + $token_digits = str_pad( dechex( $token_length ), 2, '0', STR_PAD_LEFT ); + $mapping_digits = str_pad( dechex( $mapping_length ), 2, '0', STR_PAD_LEFT ); + + $mapping = preg_replace_callback( + "~[\\x00-\\x1f\\x22\\x5c]~", + static function ( $match_result ) { + switch ( $match_result[0] ) { + case '"': + return '\\"'; + + case '\\': + return '\\\\'; + + default: + $hex = dechex( ord( $match_result[0] ) ); + return "\\x{$hex}"; + } + }, + $mapping + ); + + $comment_line .= " {$prefix}{$token}[{$mapping}]"; + $data_line .= "\\x{$token_digits}{$token}\\x{$mapping_digits}{$mapping}"; + } + $comment_line .= ".\n"; + $data_line .= "\",\n"; + + $output .= $comment_line; + $output .= $data_line; + } + + $output .= "{$i2}),\n"; + + $small_words = array(); + $small_length = strlen( $this->small_words ); + $at = 0; + while ( $at < $small_length ) { + $small_words[] = substr( $this->small_words, $at, $this->key_length + 1 ); + $at += $this->key_length + 1; + } + + $small_text = str_replace( "\x00", '\x00', implode( '', $small_words ) ); + $output .= "{$i2}\"small_words\" => \"{$small_text}\",\n"; + + $output .= "{$i2}\"small_mappings\" => array(\n"; + foreach ( $this->small_mappings as $mapping ) { + $output .= "{$i3}\"{$mapping}\",\n"; + } + $output .= "{$i2})\n"; + $output .= "{$i1})\n"; + $output .= ')'; + + return $output; + } + + /** + * Compares two strings, returning the longest, or whichever + * is first alphabetically if they are the same length. + * + * This is an important sort when building the token map because + * it should not form a match on a substring of a longer potential + * match. For example, it should not detect `Cap` when matching + * against the string `CapitalDifferentialD`. + * + * @since 6.6.0 + * + * @param string $a First string to compare. + * @param string $b Second string to compare. + * @return int -1 or lower if `$a` is less than `$b`; 1 or greater if `$a` is greater than `$b`, and 0 if they are equal. + */ + private static function longest_first_then_alphabetical( string $a, string $b ): int { + if ( $a === $b ) { + return 0; + } + + $length_a = strlen( $a ); + $length_b = strlen( $b ); + + // Longer strings are less-than for comparison's sake. + if ( $length_a !== $length_b ) { + return $length_b - $length_a; + } + + return strcmp( $a, $b ); + } +} diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-active-formatting-elements.php b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-active-formatting-elements.php new file mode 100644 index 00000000..2f51482e --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-active-formatting-elements.php @@ -0,0 +1,229 @@ + Initially, the list of active formatting elements is empty. + * > It is used to handle mis-nested formatting element tags. + * > + * > The list contains elements in the formatting category, and markers. + * > The markers are inserted when entering applet, object, marquee, + * > template, td, th, and caption elements, and are used to prevent + * > formatting from "leaking" into applet, object, marquee, template, + * > td, th, and caption elements. + * > + * > In addition, each element in the list of active formatting elements + * > is associated with the token for which it was created, so that + * > further elements can be created for that token if necessary. + * + * @since 6.4.0 + * + * @access private + * + * @see https://html.spec.whatwg.org/#list-of-active-formatting-elements + * @see WP_HTML_Processor + */ +class WP_HTML_Active_Formatting_Elements { + /** + * Holds the stack of active formatting element references. + * + * @since 6.4.0 + * + * @var WP_HTML_Token[] + */ + private $stack = array(); + + /** + * Reports if a specific node is in the stack of active formatting elements. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $token Look for this node in the stack. + * @return bool Whether the referenced node is in the stack of active formatting elements. + */ + public function contains_node( WP_HTML_Token $token ) { + foreach ( $this->walk_up() as $item ) { + if ( $token->bookmark_name === $item->bookmark_name ) { + return true; + } + } + + return false; + } + + /** + * Returns how many nodes are currently in the stack of active formatting elements. + * + * @since 6.4.0 + * + * @return int How many node are in the stack of active formatting elements. + */ + public function count() { + return count( $this->stack ); + } + + /** + * Returns the node at the end of the stack of active formatting elements, + * if one exists. If the stack is empty, returns null. + * + * @since 6.4.0 + * + * @return WP_HTML_Token|null Last node in the stack of active formatting elements, if one exists, otherwise null. + */ + public function current_node() { + $current_node = end( $this->stack ); + + return $current_node ? $current_node : null; + } + + /** + * Inserts a "marker" at the end of the list of active formatting elements. + * + * > The markers are inserted when entering applet, object, marquee, + * > template, td, th, and caption elements, and are used to prevent + * > formatting from "leaking" into applet, object, marquee, template, + * > td, th, and caption elements. + * + * @see https://html.spec.whatwg.org/#concept-parser-marker + * + * @since 6.7.0 + */ + public function insert_marker(): void { + $this->push( new WP_HTML_Token( null, 'marker', false ) ); + } + + /** + * Pushes a node onto the stack of active formatting elements. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#push-onto-the-list-of-active-formatting-elements + * + * @param WP_HTML_Token $token Push this node onto the stack. + */ + public function push( WP_HTML_Token $token ) { + /* + * > If there are already three elements in the list of active formatting elements after the last marker, + * > if any, or anywhere in the list if there are no markers, that have the same tag name, namespace, and + * > attributes as element, then remove the earliest such element from the list of active formatting + * > elements. For these purposes, the attributes must be compared as they were when the elements were + * > created by the parser; two elements have the same attributes if all their parsed attributes can be + * > paired such that the two attributes in each pair have identical names, namespaces, and values + * > (the order of the attributes does not matter). + * + * @todo Implement the "Noah's Ark clause" to only add up to three of any given kind of formatting elements to the stack. + */ + // > Add element to the list of active formatting elements. + $this->stack[] = $token; + } + + /** + * Removes a node from the stack of active formatting elements. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $token Remove this node from the stack, if it's there already. + * @return bool Whether the node was found and removed from the stack of active formatting elements. + */ + public function remove_node( WP_HTML_Token $token ) { + foreach ( $this->walk_up() as $position_from_end => $item ) { + if ( $token->bookmark_name !== $item->bookmark_name ) { + continue; + } + + $position_from_start = $this->count() - $position_from_end - 1; + array_splice( $this->stack, $position_from_start, 1 ); + return true; + } + + return false; + } + + /** + * Steps through the stack of active formatting elements, starting with the + * top element (added first) and walking downwards to the one added last. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $html = 'We are here'; + * foreach ( $stack->walk_down() as $node ) { + * echo "{$node->node_name} -> "; + * } + * > EM -> STRONG -> A -> + * + * To start with the most-recently added element and walk towards the top, + * see WP_HTML_Active_Formatting_Elements::walk_up(). + * + * @since 6.4.0 + */ + public function walk_down() { + $count = count( $this->stack ); + + for ( $i = 0; $i < $count; $i++ ) { + yield $this->stack[ $i ]; + } + } + + /** + * Steps through the stack of active formatting elements, starting with the + * bottom element (added last) and walking upwards to the one added first. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $html = 'We are here'; + * foreach ( $stack->walk_up() as $node ) { + * echo "{$node->node_name} -> "; + * } + * > A -> STRONG -> EM -> + * + * To start with the first added element and walk towards the bottom, + * see WP_HTML_Active_Formatting_Elements::walk_down(). + * + * @since 6.4.0 + */ + public function walk_up() { + for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) { + yield $this->stack[ $i ]; + } + } + + /** + * Clears the list of active formatting elements up to the last marker. + * + * > When the steps below require the UA to clear the list of active formatting elements up to + * > the last marker, the UA must perform the following steps: + * > + * > 1. Let entry be the last (most recently added) entry in the list of active + * > formatting elements. + * > 2. Remove entry from the list of active formatting elements. + * > 3. If entry was a marker, then stop the algorithm at this point. + * > The list has been cleared up to the last marker. + * > 4. Go to step 1. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-list-of-active-formatting-elements-up-to-the-last-marker + * + * @since 6.7.0 + */ + public function clear_up_to_last_marker(): void { + foreach ( $this->walk_up() as $item ) { + array_pop( $this->stack ); + if ( 'marker' === $item->node_name ) { + break; + } + } + } +} diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-attribute-token.php b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-attribute-token.php new file mode 100644 index 00000000..74d41320 --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-attribute-token.php @@ -0,0 +1,116 @@ + + * ------------ length is 12, including quotes + * + * + * ------- length is 6 + * + * + * ------------ length is 11 + * + * @since 6.5.0 Replaced `end` with `length` to more closely match `substr()`. + * + * @var int + */ + public $length; + + /** + * Whether the attribute is a boolean attribute with value `true`. + * + * @since 6.2.0 + * + * @var bool + */ + public $is_true; + + /** + * Constructor. + * + * @since 6.2.0 + * @since 6.5.0 Replaced `end` with `length` to more closely match `substr()`. + * + * @param string $name Attribute name. + * @param int $value_start Attribute value. + * @param int $value_length Number of bytes attribute value spans. + * @param int $start The string offset where the attribute name starts. + * @param int $length Byte length of the entire attribute name or name and value pair expression. + * @param bool $is_true Whether the attribute is a boolean attribute with true value. + */ + public function __construct( $name, $value_start, $value_length, $start, $length, $is_true ) { + $this->name = $name; + $this->value_starts_at = $value_start; + $this->value_length = $value_length; + $this->start = $start; + $this->length = $length; + $this->is_true = $is_true; + } +} diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-decoder.php b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-decoder.php new file mode 100644 index 00000000..6c1404be --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-decoder.php @@ -0,0 +1,463 @@ += $length ) { + return null; + } + + if ( '&' !== $text[ $at ] ) { + return null; + } + + /* + * Numeric character references. + * + * When truncated, these will encode the code point found by parsing the + * digits that are available. For example, when `🅰` is truncated + * to `DZ` it will encode `DZ`. It does not: + * - know how to parse the original `🅰`. + * - fail to parse and return plaintext `DZ`. + * - fail to parse and return the replacement character `�` + */ + if ( '#' === $text[ $at + 1 ] ) { + if ( $at + 2 >= $length ) { + return null; + } + + /** Tracks inner parsing within the numeric character reference. */ + $digits_at = $at + 2; + + if ( 'x' === $text[ $digits_at ] || 'X' === $text[ $digits_at ] ) { + $numeric_base = 16; + $numeric_digits = '0123456789abcdefABCDEF'; + $max_digits = 6; // 􏿿 + ++$digits_at; + } else { + $numeric_base = 10; + $numeric_digits = '0123456789'; + $max_digits = 7; // 􏿿 + } + + // Cannot encode invalid Unicode code points. Max is to U+10FFFF. + $zero_count = strspn( $text, '0', $digits_at ); + $digit_count = strspn( $text, $numeric_digits, $digits_at + $zero_count ); + $after_digits = $digits_at + $zero_count + $digit_count; + $has_semicolon = $after_digits < $length && ';' === $text[ $after_digits ]; + $end_of_span = $has_semicolon ? $after_digits + 1 : $after_digits; + + // `&#` or `&#x` without digits returns into plaintext. + if ( 0 === $digit_count && 0 === $zero_count ) { + return null; + } + + // Whereas `&#` and only zeros is invalid. + if ( 0 === $digit_count ) { + $match_byte_length = $end_of_span - $at; + return '�'; + } + + // If there are too many digits then it's not worth parsing. It's invalid. + if ( $digit_count > $max_digits ) { + $match_byte_length = $end_of_span - $at; + return '�'; + } + + $digits = substr( $text, $digits_at + $zero_count, $digit_count ); + $code_point = intval( $digits, $numeric_base ); + + /* + * Noncharacters, 0x0D, and non-ASCII-whitespace control characters. + * + * > A noncharacter is a code point that is in the range U+FDD0 to U+FDEF, + * > inclusive, or U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, + * > U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, + * > U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, + * > U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE, + * > U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, or U+10FFFF. + * + * A C0 control is a code point that is in the range of U+00 to U+1F, + * but ASCII whitespace includes U+09, U+0A, U+0C, and U+0D. + * + * These characters are invalid but still decode as any valid character. + * This comment is here to note and explain why there's no check to + * remove these characters or replace them. + * + * @see https://infra.spec.whatwg.org/#noncharacter + */ + + /* + * Code points in the C1 controls area need to be remapped as if they + * were stored in Windows-1252. Note! This transformation only happens + * for numeric character references. The raw code points in the byte + * stream are not translated. + * + * > If the number is one of the numbers in the first column of + * > the following table, then find the row with that number in + * > the first column, and set the character reference code to + * > the number in the second column of that row. + */ + if ( $code_point >= 0x80 && $code_point <= 0x9F ) { + $windows_1252_mapping = array( + 0x20AC, // 0x80 -> EURO SIGN (€). + 0x81, // 0x81 -> (no change). + 0x201A, // 0x82 -> SINGLE LOW-9 QUOTATION MARK (‚). + 0x0192, // 0x83 -> LATIN SMALL LETTER F WITH HOOK (ƒ). + 0x201E, // 0x84 -> DOUBLE LOW-9 QUOTATION MARK („). + 0x2026, // 0x85 -> HORIZONTAL ELLIPSIS (…). + 0x2020, // 0x86 -> DAGGER (†). + 0x2021, // 0x87 -> DOUBLE DAGGER (‡). + 0x02C6, // 0x88 -> MODIFIER LETTER CIRCUMFLEX ACCENT (ˆ). + 0x2030, // 0x89 -> PER MILLE SIGN (‰). + 0x0160, // 0x8A -> LATIN CAPITAL LETTER S WITH CARON (Š). + 0x2039, // 0x8B -> SINGLE LEFT-POINTING ANGLE QUOTATION MARK (‹). + 0x0152, // 0x8C -> LATIN CAPITAL LIGATURE OE (Œ). + 0x8D, // 0x8D -> (no change). + 0x017D, // 0x8E -> LATIN CAPITAL LETTER Z WITH CARON (Ž). + 0x8F, // 0x8F -> (no change). + 0x90, // 0x90 -> (no change). + 0x2018, // 0x91 -> LEFT SINGLE QUOTATION MARK (‘). + 0x2019, // 0x92 -> RIGHT SINGLE QUOTATION MARK (’). + 0x201C, // 0x93 -> LEFT DOUBLE QUOTATION MARK (“). + 0x201D, // 0x94 -> RIGHT DOUBLE QUOTATION MARK (”). + 0x2022, // 0x95 -> BULLET (•). + 0x2013, // 0x96 -> EN DASH (–). + 0x2014, // 0x97 -> EM DASH (—). + 0x02DC, // 0x98 -> SMALL TILDE (˜). + 0x2122, // 0x99 -> TRADE MARK SIGN (™). + 0x0161, // 0x9A -> LATIN SMALL LETTER S WITH CARON (š). + 0x203A, // 0x9B -> SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (›). + 0x0153, // 0x9C -> LATIN SMALL LIGATURE OE (œ). + 0x9D, // 0x9D -> (no change). + 0x017E, // 0x9E -> LATIN SMALL LETTER Z WITH CARON (ž). + 0x0178, // 0x9F -> LATIN CAPITAL LETTER Y WITH DIAERESIS (Ÿ). + ); + + $code_point = $windows_1252_mapping[ $code_point - 0x80 ]; + } + + $match_byte_length = $end_of_span - $at; + return self::code_point_to_utf8_bytes( $code_point ); + } + + /** Tracks inner parsing within the named character reference. */ + $name_at = $at + 1; + // Minimum named character reference is two characters. E.g. `GT`. + if ( $name_at + 2 > $length ) { + return null; + } + + $name_length = 0; + $replacement = $html5_named_character_references->read_token( $text, $name_at, $name_length ); + if ( false === $replacement ) { + return null; + } + + $after_name = $name_at + $name_length; + + // If the match ended with a semicolon then it should always be decoded. + if ( ';' === $text[ $name_at + $name_length - 1 ] ) { + $match_byte_length = $after_name - $at; + return $replacement; + } + + /* + * At this point though there's a match for an entry in the named + * character reference table but the match doesn't end in `;`. + * It may be allowed if it's followed by something unambiguous. + */ + $ambiguous_follower = ( + $after_name < $length && + $name_at < $length && + ( + ctype_alnum( $text[ $after_name ] ) || + '=' === $text[ $after_name ] + ) + ); + + // It's non-ambiguous, safe to leave it in. + if ( ! $ambiguous_follower ) { + $match_byte_length = $after_name - $at; + return $replacement; + } + + // It's ambiguous, which isn't allowed inside attributes. + if ( 'attribute' === $context ) { + return null; + } + + $match_byte_length = $after_name - $at; + return $replacement; + } + + /** + * Encode a code point number into the UTF-8 encoding. + * + * This encoder implements the UTF-8 encoding algorithm for converting + * a code point into a byte sequence. If it receives an invalid code + * point it will return the Unicode Replacement Character U+FFFD `�`. + * + * Example: + * + * '🅰' === WP_HTML_Decoder::code_point_to_utf8_bytes( 0x1f170 ); + * + * // Half of a surrogate pair is an invalid code point. + * '�' === WP_HTML_Decoder::code_point_to_utf8_bytes( 0xd83c ); + * + * @since 6.6.0 + * + * @see https://www.rfc-editor.org/rfc/rfc3629 For the UTF-8 standard. + * + * @param int $code_point Which code point to convert. + * @return string Converted code point, or `�` if invalid. + */ + public static function code_point_to_utf8_bytes( $code_point ): string { + // Pre-check to ensure a valid code point. + if ( + $code_point <= 0 || + ( $code_point >= 0xD800 && $code_point <= 0xDFFF ) || + $code_point > 0x10FFFF + ) { + return '�'; + } + + if ( $code_point <= 0x7F ) { + return chr( $code_point ); + } + + if ( $code_point <= 0x7FF ) { + $byte1 = chr( ( $code_point >> 6 ) | 0xC0 ); + $byte2 = chr( $code_point & 0x3F | 0x80 ); + + return "{$byte1}{$byte2}"; + } + + if ( $code_point <= 0xFFFF ) { + $byte1 = chr( ( $code_point >> 12 ) | 0xE0 ); + $byte2 = chr( ( $code_point >> 6 ) & 0x3F | 0x80 ); + $byte3 = chr( $code_point & 0x3F | 0x80 ); + + return "{$byte1}{$byte2}{$byte3}"; + } + + // Any values above U+10FFFF are eliminated above in the pre-check. + $byte1 = chr( ( $code_point >> 18 ) | 0xF0 ); + $byte2 = chr( ( $code_point >> 12 ) & 0x3F | 0x80 ); + $byte3 = chr( ( $code_point >> 6 ) & 0x3F | 0x80 ); + $byte4 = chr( $code_point & 0x3F | 0x80 ); + + return "{$byte1}{$byte2}{$byte3}{$byte4}"; + } +} diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-doctype-info.php b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-doctype-info.php new file mode 100644 index 00000000..e0396f7d --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-doctype-info.php @@ -0,0 +1,616 @@ +`. + * + * > DOCTYPEs are required for legacy reasons. When omitted, browsers tend to use a different + * > rendering mode that is incompatible with some specifications. Including the DOCTYPE in a + * > document ensures that the browser makes a best-effort attempt at following the + * > relevant specifications. + * + * @see https://html.spec.whatwg.org/#the-doctype + * + * DOCTYPE declarations comprise four properties: a name, public identifier, system identifier, + * and an indication of which document compatability mode they would imply if an HTML parser + * hadn't already determined it from other information. + * + * @see https://html.spec.whatwg.org/#the-initial-insertion-mode + * + * Historically, the DOCTYPE declaration was used in SGML documents to instruct a parser how + * to interpret the various tags and entities within a document. Its role in HTML diverged + * from how it was used in SGML and no meaning should be back-read into HTML based on how it + * is used in SGML, XML, or XHTML documents. + * + * @see https://www.iso.org/standard/16387.html + * + * @since 6.7.0 + * + * @see WP_HTML_Processor + */ +class WP_HTML_Doctype_Info { + /** + * Name of the DOCTYPE: should be "html" for HTML documents. + * + * This value should be considered "read only" and not modified. + * + * Historically the DOCTYPE name indicates name of the document's root element. + * + * + * ╰──┴── name is "html". + * + * @see https://html.spec.whatwg.org/#tokenization + * + * @since 6.7.0 + * + * @var string|null + */ + public $name = null; + + /** + * Public identifier of the DOCTYPE. + * + * This value should be considered "read only" and not modified. + * + * The public identifier is optional and should not appear in HTML documents. + * A `null` value indicates that no public identifier was present in the DOCTYPE. + * + * Historically the presence of the public identifier indicated that a document + * was meant to be shared between computer systems and the value indicated to a + * knowledgeable parser how to find the relevant document type definition (DTD). + * + * + * │ │ ╰─── public identifier ─────╯ + * ╰──┴── name is "html". + * + * @see https://html.spec.whatwg.org/#tokenization + * + * @since 6.7.0 + * + * @var string|null + */ + public $public_identifier = null; + + /** + * System identifier of the DOCTYPE. + * + * This value should be considered "read only" and not modified. + * + * The system identifier is optional and should not appear in HTML documents. + * A `null` value indicates that no system identifier was present in the DOCTYPE. + * + * Historically the system identifier specified where a relevant document type + * declaration for the given document is stored and may be retrieved. + * + * + * │ │ ╰──── system identifier ────╯ + * ╰──┴── name is "html". + * + * If a public identifier were provided it would indicate to a knowledgeable + * parser how to interpret the system identifier. + * + * + * │ │ ╰─── public identifier ─────╯ ╰──── system identifier ────╯ + * ╰──┴── name is "html". + * + * @see https://html.spec.whatwg.org/#tokenization + * + * @since 6.7.0 + * + * @var string|null + */ + public $system_identifier = null; + + /** + * Which document compatability mode this DOCTYPE declaration indicates. + * + * This value should be considered "read only" and not modified. + * + * When an HTML parser has not already set the document compatability mode, + * (e.g. "quirks" or "no-quirks" mode), it will infer if from the properties + * of the appropriate DOCTYPE declaration, if one exists. The DOCTYPE can + * indicate one of three possible document compatability modes: + * + * - "no-quirks" and "limited-quirks" modes (also called "standards" mode). + * - "quirks" mode (also called `CSS1Compat` mode). + * + * An appropriate DOCTYPE is one encountered in the "initial" insertion mode, + * before the HTML element has been opened and before finding any other + * DOCTYPE declaration tokens. + * + * @see https://html.spec.whatwg.org/#the-initial-insertion-mode + * + * @since 6.7.0 + * + * @var string One of "no-quirks", "limited-quirks", or "quirks". + */ + public $indicated_compatability_mode; + + /** + * Constructor. + * + * This class should not be instantiated directly. + * Use the static {@see self::from_doctype_token} method instead. + * + * The arguments to this constructor correspond to the "DOCTYPE token" + * as defined in the HTML specification. + * + * > DOCTYPE tokens have a name, a public identifier, a system identifier, + * > and a force-quirks flag. When a DOCTYPE token is created, its name, public identifier, + * > and system identifier must be marked as missing (which is a distinct state from the + * > empty string), and the force-quirks flag must be set to off (its other state is on). + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#tokenization + * + * @since 6.7.0 + * + * @param string|null $name Name of the DOCTYPE. + * @param string|null $public_identifier Public identifier of the DOCTYPE. + * @param string|null $system_identifier System identifier of the DOCTYPE. + * @param bool $force_quirks_flag Whether the force-quirks flag is set for the token. + */ + private function __construct( + ?string $name, + ?string $public_identifier, + ?string $system_identifier, + bool $force_quirks_flag + ) { + $this->name = $name; + $this->public_identifier = $public_identifier; + $this->system_identifier = $system_identifier; + + /* + * > If the DOCTYPE token matches one of the conditions in the following list, + * > then set the Document to quirks mode: + */ + + /* + * > The force-quirks flag is set to on. + */ + if ( $force_quirks_flag ) { + $this->indicated_compatability_mode = 'quirks'; + return; + } + + /* + * Normative documents will contain the literal `` with no + * public or system identifiers; short-circuit to avoid extra parsing. + */ + if ( 'html' === $name && null === $public_identifier && null === $system_identifier ) { + $this->indicated_compatability_mode = 'no-quirks'; + return; + } + + /* + * > The name is not "html". + * + * The tokenizer must report the name in lower case even if provided in + * the document in upper case; thus no conversion is required here. + */ + if ( 'html' !== $name ) { + $this->indicated_compatability_mode = 'quirks'; + return; + } + + /* + * Set up some variables to handle the rest of the conditions. + * + * > set...the public identifier...to...the empty string if the public identifier was missing. + * > set...the system identifier...to...the empty string if the system identifier was missing. + * > + * > The system identifier and public identifier strings must be compared... + * > in an ASCII case-insensitive manner. + * > + * > A system identifier whose value is the empty string is not considered missing + * > for the purposes of the conditions above. + */ + $system_identifier_is_missing = null === $system_identifier; + $public_identifier = null === $public_identifier ? '' : strtolower( $public_identifier ); + $system_identifier = null === $system_identifier ? '' : strtolower( $system_identifier ); + + /* + * > The public identifier is set to… + */ + if ( + '-//w3o//dtd w3 html strict 3.0//en//' === $public_identifier || + '-/w3c/dtd html 4.0 transitional/en' === $public_identifier || + 'html' === $public_identifier + ) { + $this->indicated_compatability_mode = 'quirks'; + return; + } + + /* + * > The system identifier is set to… + */ + if ( 'http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd' === $system_identifier ) { + $this->indicated_compatability_mode = 'quirks'; + return; + } + + /* + * All of the following conditions depend on matching the public identifier. + * If the public identifier is empty, none of the following conditions will match. + */ + if ( '' === $public_identifier ) { + $this->indicated_compatability_mode = 'no-quirks'; + return; + } + + /* + * > The public identifier starts with… + * + * @todo Optimize this matching. It shouldn't be a large overall performance issue, + * however, as only a single DOCTYPE declaration token should ever be parsed, + * and normative documents will have exited before reaching this condition. + */ + if ( + str_starts_with( $public_identifier, '+//silmaril//dtd html pro v0r11 19970101//' ) || + str_starts_with( $public_identifier, '-//as//dtd html 3.0 aswedit + extensions//' ) || + str_starts_with( $public_identifier, '-//advasoft ltd//dtd html 3.0 aswedit + extensions//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 level 1//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 level 2//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict level 1//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict level 2//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 2.0//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 2.1e//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 3.0//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 3.2 final//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 3.2//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html 3//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html level 0//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html level 1//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html level 2//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html level 3//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html strict level 0//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html strict level 1//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html strict level 2//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html strict level 3//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html strict//' ) || + str_starts_with( $public_identifier, '-//ietf//dtd html//' ) || + str_starts_with( $public_identifier, '-//metrius//dtd metrius presentational//' ) || + str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 html strict//' ) || + str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 html//' ) || + str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 tables//' ) || + str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 html strict//' ) || + str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 html//' ) || + str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 tables//' ) || + str_starts_with( $public_identifier, '-//netscape comm. corp.//dtd html//' ) || + str_starts_with( $public_identifier, '-//netscape comm. corp.//dtd strict html//' ) || + str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html 2.0//" ) || + str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html extended 1.0//" ) || + str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html extended relaxed 1.0//" ) || + str_starts_with( $public_identifier, '-//sq//dtd html 2.0 hotmetal + extensions//' ) || + str_starts_with( $public_identifier, '-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//' ) || + str_starts_with( $public_identifier, '-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//' ) || + str_starts_with( $public_identifier, '-//spyglass//dtd html 2.0 extended//' ) || + str_starts_with( $public_identifier, '-//sun microsystems corp.//dtd hotjava html//' ) || + str_starts_with( $public_identifier, '-//sun microsystems corp.//dtd hotjava strict html//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 3 1995-03-24//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 3.2 draft//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 3.2 final//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 3.2//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 3.2s draft//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 4.0 frameset//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 4.0 transitional//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html experimental 19960712//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html experimental 970421//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd w3 html//' ) || + str_starts_with( $public_identifier, '-//w3o//dtd w3 html 3.0//' ) || + str_starts_with( $public_identifier, '-//webtechs//dtd mozilla html 2.0//' ) || + str_starts_with( $public_identifier, '-//webtechs//dtd mozilla html//' ) + ) { + $this->indicated_compatability_mode = 'quirks'; + return; + } + + /* + * > The system identifier is missing and the public identifier starts with… + */ + if ( + $system_identifier_is_missing && ( + str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 frameset//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 transitional//' ) + ) + ) { + $this->indicated_compatability_mode = 'quirks'; + return; + } + + /* + * > Otherwise, if the DOCTYPE token matches one of the conditions in + * > the following list, then set the Document to limited-quirks mode. + */ + + /* + * > The public identifier starts with… + */ + if ( + str_starts_with( $public_identifier, '-//w3c//dtd xhtml 1.0 frameset//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd xhtml 1.0 transitional//' ) + ) { + $this->indicated_compatability_mode = 'limited-quirks'; + return; + } + + /* + * > The system identifier is not missing and the public identifier starts with… + */ + if ( + ! $system_identifier_is_missing && ( + str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 frameset//' ) || + str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 transitional//' ) + ) + ) { + $this->indicated_compatability_mode = 'limited-quirks'; + return; + } + + $this->indicated_compatability_mode = 'no-quirks'; + } + + /** + * Creates a WP_HTML_Doctype_Info instance by parsing a raw DOCTYPE declaration token. + * + * Use this method to parse a DOCTYPE declaration token and get access to its properties + * via the returned WP_HTML_Doctype_Info class instance. The provided input must parse + * properly as a DOCTYPE declaration, though it must not represent a valid DOCTYPE. + * + * Example: + * + * // Normative HTML DOCTYPE declaration. + * $doctype = WP_HTML_Doctype_Info::from_doctype_token( '' ); + * 'no-quirks' === $doctype->indicated_compatability_mode; + * + * // A nonsensical DOCTYPE is still valid, and will indicate "quirks" mode. + * $doctype = WP_HTML_Doctype_Info::from_doctype_token( '' ); + * 'quirks' === $doctype->indicated_compatability_mode; + * + * // Textual quirks present in raw HTML are handled appropriately. + * $doctype = WP_HTML_Doctype_Info::from_doctype_token( "" ); + * 'no-quirks' === $doctype->indicated_compatability_mode; + * + * // Anything other than a proper DOCTYPE declaration token fails to parse. + * null === WP_HTML_Doctype_Info::from_doctype_token( ' ' ); + * null === WP_HTML_Doctype_Info::from_doctype_token( '

' ); + * null === WP_HTML_Doctype_Info::from_doctype_token( '' ); + * null === WP_HTML_Doctype_Info::from_doctype_token( 'html' ); + * null === WP_HTML_Doctype_Info::from_doctype_token( '' ); + * + * @since 6.7.0 + * + * @param string $doctype_html The complete raw DOCTYPE HTML string, e.g. ``. + * + * @return WP_HTML_Doctype_Info|null A WP_HTML_Doctype_Info instance will be returned if the + * provided DOCTYPE HTML is a valid DOCTYPE. Otherwise, null. + */ + public static function from_doctype_token( string $doctype_html ): ?self { + $doctype_name = null; + $doctype_public_id = null; + $doctype_system_id = null; + + $end = strlen( $doctype_html ) - 1; + + /* + * This parser combines the rules for parsing DOCTYPE tokens found in the HTML + * specification for the DOCTYPE related tokenizer states. + * + * @see https://html.spec.whatwg.org/#doctype-state + */ + + /* + * - Valid DOCTYPE HTML token must be at least `` assuming a complete token not + * ending in end-of-file. + * - It must start with an ASCII case-insensitive match for `` must be the final byte in the HTML string. + */ + if ( + $end < 9 || + 0 !== substr_compare( $doctype_html, '`? + if ( '>' !== $doctype_html[ $end ] || ( strcspn( $doctype_html, '>', $at ) + $at ) < $end ) { + return null; + } + + /* + * Perform newline normalization and ensure the $end value is correct after normalization. + * + * @see https://html.spec.whatwg.org/#preprocessing-the-input-stream + * @see https://infra.spec.whatwg.org/#normalize-newlines + */ + $doctype_html = str_replace( "\r\n", "\n", $doctype_html ); + $doctype_html = str_replace( "\r", "\n", $doctype_html ); + $end = strlen( $doctype_html ) - 1; + + /* + * In this state, the doctype token has been found and its "content" optionally including the + * name, public identifier, and system identifier is between the current position and the end. + * + * "" + * ╰─ $at ╰─ $end + * + * It's also possible that the declaration part is empty. + * + * ╭─ $at + * "" + * ╰─ $end + * + * Rules for parsing ">" which terminates the DOCTYPE do not need to be considered as they + * have been handled above in the condition that the provided DOCTYPE HTML must contain + * exactly one ">" character in the final position. + */ + + /* + * + * Parsing effectively begins in "Before DOCTYPE name state". Ignore whitespace and + * proceed to the next state. + * + * @see https://html.spec.whatwg.org/#before-doctype-name-state + */ + $at += strspn( $doctype_html, " \t\n\f\r", $at ); + + if ( $at >= $end ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + } + + $name_length = strcspn( $doctype_html, " \t\n\f\r", $at, $end - $at ); + $doctype_name = str_replace( "\0", "\u{FFFD}", strtolower( substr( $doctype_html, $at, $name_length ) ) ); + + $at += $name_length; + $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at ); + if ( $at >= $end ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false ); + } + + /* + * "After DOCTYPE name state" + * + * Find a case-insensitive match for "PUBLIC" or "SYSTEM" at this point. + * Otherwise, set force-quirks and enter bogus DOCTYPE state (skip the rest of the doctype). + * + * @see https://html.spec.whatwg.org/#after-doctype-name-state + */ + if ( $at + 6 >= $end ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + } + + /* + * > If the six characters starting from the current input character are an ASCII + * > case-insensitive match for the word "PUBLIC", then consume those characters + * > and switch to the after DOCTYPE public keyword state. + */ + if ( 0 === substr_compare( $doctype_html, 'PUBLIC', $at, 6, true ) ) { + $at += 6; + $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at ); + if ( $at >= $end ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + } + goto parse_doctype_public_identifier; + } + + /* + * > Otherwise, if the six characters starting from the current input character are an ASCII + * > case-insensitive match for the word "SYSTEM", then consume those characters and switch + * > to the after DOCTYPE system keyword state. + */ + if ( 0 === substr_compare( $doctype_html, 'SYSTEM', $at, 6, true ) ) { + $at += 6; + $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at ); + if ( $at >= $end ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + } + goto parse_doctype_system_identifier; + } + + /* + * > Otherwise, this is an invalid-character-sequence-after-doctype-name parse error. + * > Set the current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus + * > DOCTYPE state. + */ + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + + parse_doctype_public_identifier: + /* + * The parser should enter "DOCTYPE public identifier (double-quoted) state" or + * "DOCTYPE public identifier (single-quoted) state" by finding one of the valid quotes. + * Anything else forces quirks mode and ignores the rest of the contents. + * + * @see https://html.spec.whatwg.org/#doctype-public-identifier-(double-quoted)-state + * @see https://html.spec.whatwg.org/#doctype-public-identifier-(single-quoted)-state + */ + $closer_quote = $doctype_html[ $at ]; + + /* + * > This is a missing-quote-before-doctype-public-identifier parse error. Set the + * > current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus DOCTYPE state. + */ + if ( '"' !== $closer_quote && "'" !== $closer_quote ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + } + + ++$at; + + $identifier_length = strcspn( $doctype_html, $closer_quote, $at, $end - $at ); + $doctype_public_id = str_replace( "\0", "\u{FFFD}", substr( $doctype_html, $at, $identifier_length ) ); + + $at += $identifier_length; + if ( $at >= $end || $closer_quote !== $doctype_html[ $at ] ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + } + + ++$at; + + /* + * "Between DOCTYPE public and system identifiers state" + * + * Advance through whitespace between public and system identifiers. + * + * @see https://html.spec.whatwg.org/#between-doctype-public-and-system-identifiers-state + */ + $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at ); + if ( $at >= $end ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false ); + } + + parse_doctype_system_identifier: + /* + * The parser should enter "DOCTYPE system identifier (double-quoted) state" or + * "DOCTYPE system identifier (single-quoted) state" by finding one of the valid quotes. + * Anything else forces quirks mode and ignores the rest of the contents. + * + * @see https://html.spec.whatwg.org/#doctype-system-identifier-(double-quoted)-state + * @see https://html.spec.whatwg.org/#doctype-system-identifier-(single-quoted)-state + */ + $closer_quote = $doctype_html[ $at ]; + + /* + * > This is a missing-quote-before-doctype-system-identifier parse error. Set the + * > current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus DOCTYPE state. + */ + if ( '"' !== $closer_quote && "'" !== $closer_quote ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + } + + ++$at; + + $identifier_length = strcspn( $doctype_html, $closer_quote, $at, $end - $at ); + $doctype_system_id = str_replace( "\0", "\u{FFFD}", substr( $doctype_html, $at, $identifier_length ) ); + + $at += $identifier_length; + if ( $at >= $end || $closer_quote !== $doctype_html[ $at ] ) { + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true ); + } + + return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false ); + } +} diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-open-elements.php b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-open-elements.php new file mode 100644 index 00000000..210492ab --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-open-elements.php @@ -0,0 +1,852 @@ + Initially, the stack of open elements is empty. The stack grows + * > downwards; the topmost node on the stack is the first one added + * > to the stack, and the bottommost node of the stack is the most + * > recently added node in the stack (notwithstanding when the stack + * > is manipulated in a random access fashion as part of the handling + * > for misnested tags). + * + * @since 6.4.0 + * + * @access private + * + * @see https://html.spec.whatwg.org/#stack-of-open-elements + * @see WP_HTML_Processor + */ +class WP_HTML_Open_Elements { + /** + * Holds the stack of open element references. + * + * @since 6.4.0 + * + * @var WP_HTML_Token[] + */ + public $stack = array(); + + /** + * Whether a P element is in button scope currently. + * + * This class optimizes scope lookup by pre-calculating + * this value when elements are added and removed to the + * stack of open elements which might change its value. + * This avoids frequent iteration over the stack. + * + * @since 6.4.0 + * + * @var bool + */ + private $has_p_in_button_scope = false; + + /** + * A function that will be called when an item is popped off the stack of open elements. + * + * The function will be called with the popped item as its argument. + * + * @since 6.6.0 + * + * @var Closure|null + */ + private $pop_handler = null; + + /** + * A function that will be called when an item is pushed onto the stack of open elements. + * + * The function will be called with the pushed item as its argument. + * + * @since 6.6.0 + * + * @var Closure|null + */ + private $push_handler = null; + + /** + * Sets a pop handler that will be called when an item is popped off the stack of + * open elements. + * + * The function will be called with the pushed item as its argument. + * + * @since 6.6.0 + * + * @param Closure $handler The handler function. + */ + public function set_pop_handler( Closure $handler ): void { + $this->pop_handler = $handler; + } + + /** + * Sets a push handler that will be called when an item is pushed onto the stack of + * open elements. + * + * The function will be called with the pushed item as its argument. + * + * @since 6.6.0 + * + * @param Closure $handler The handler function. + */ + public function set_push_handler( Closure $handler ): void { + $this->push_handler = $handler; + } + + /** + * Returns the name of the node at the nth position on the stack + * of open elements, or `null` if no such position exists. + * + * Note that this uses a 1-based index, which represents the + * "nth item" on the stack, counting from the top, where the + * top-most element is the 1st, the second is the 2nd, etc... + * + * @since 6.7.0 + * + * @param int $nth Retrieve the nth item on the stack, with 1 being + * the top element, 2 being the second, etc... + * @return WP_HTML_Token|null Name of the node on the stack at the given location, + * or `null` if the location isn't on the stack. + */ + public function at( int $nth ): ?WP_HTML_Token { + foreach ( $this->walk_down() as $item ) { + if ( 0 === --$nth ) { + return $item; + } + } + + return null; + } + + /** + * Reports if a node of a given name is in the stack of open elements. + * + * @since 6.7.0 + * + * @param string $node_name Name of node for which to check. + * @return bool Whether a node of the given name is in the stack of open elements. + */ + public function contains( string $node_name ): bool { + foreach ( $this->walk_up() as $item ) { + if ( $node_name === $item->node_name ) { + return true; + } + } + + return false; + } + + /** + * Reports if a specific node is in the stack of open elements. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $token Look for this node in the stack. + * @return bool Whether the referenced node is in the stack of open elements. + */ + public function contains_node( WP_HTML_Token $token ): bool { + foreach ( $this->walk_up() as $item ) { + if ( $token === $item ) { + return true; + } + } + + return false; + } + + /** + * Returns how many nodes are currently in the stack of open elements. + * + * @since 6.4.0 + * + * @return int How many node are in the stack of open elements. + */ + public function count(): int { + return count( $this->stack ); + } + + /** + * Returns the node at the end of the stack of open elements, + * if one exists. If the stack is empty, returns null. + * + * @since 6.4.0 + * + * @return WP_HTML_Token|null Last node in the stack of open elements, if one exists, otherwise null. + */ + public function current_node(): ?WP_HTML_Token { + $current_node = end( $this->stack ); + + return $current_node ? $current_node : null; + } + + /** + * Indicates if the current node is of a given type or name. + * + * It's possible to pass either a node type or a node name to this function. + * In the case there is no current element it will always return `false`. + * + * Example: + * + * // Is the current node a text node? + * $stack->current_node_is( '#text' ); + * + * // Is the current node a DIV element? + * $stack->current_node_is( 'DIV' ); + * + * // Is the current node any element/tag? + * $stack->current_node_is( '#tag' ); + * + * @see WP_HTML_Tag_Processor::get_token_type + * @see WP_HTML_Tag_Processor::get_token_name + * + * @since 6.7.0 + * + * @access private + * + * @param string $identity Check if the current node has this name or type (depending on what is provided). + * @return bool Whether there is a current element that matches the given identity, whether a token name or type. + */ + public function current_node_is( string $identity ): bool { + $current_node = end( $this->stack ); + if ( false === $current_node ) { + return false; + } + + $current_node_name = $current_node->node_name; + + return ( + $current_node_name === $identity || + ( '#doctype' === $identity && 'html' === $current_node_name ) || + ( '#tag' === $identity && ctype_upper( $current_node_name ) ) + ); + } + + /** + * Returns whether an element is in a specific scope. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope + * + * @param string $tag_name Name of tag check. + * @param string[] $termination_list List of elements that terminate the search. + * @return bool Whether the element was found in a specific scope. + */ + public function has_element_in_specific_scope( string $tag_name, $termination_list ): bool { + foreach ( $this->walk_up() as $node ) { + $namespaced_name = 'html' === $node->namespace + ? $node->node_name + : "{$node->namespace} {$node->node_name}"; + + if ( $namespaced_name === $tag_name ) { + return true; + } + + if ( + '(internal: H1 through H6 - do not use)' === $tag_name && + in_array( $namespaced_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) + ) { + return true; + } + + if ( in_array( $namespaced_name, $termination_list, true ) ) { + return false; + } + } + + return false; + } + + /** + * Returns whether a particular element is in scope. + * + * > The stack of open elements is said to have a particular element in + * > scope when it has that element in the specific scope consisting of + * > the following element types: + * > + * > - applet + * > - caption + * > - html + * > - table + * > - td + * > - th + * > - marquee + * > - object + * > - template + * > - MathML mi + * > - MathML mo + * > - MathML mn + * > - MathML ms + * > - MathML mtext + * > - MathML annotation-xml + * > - SVG foreignObject + * > - SVG desc + * > - SVG title + * + * @since 6.4.0 + * @since 6.7.0 Full support. + * + * @see https://html.spec.whatwg.org/#has-an-element-in-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether given element is in scope. + */ + public function has_element_in_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( + $tag_name, + array( + 'APPLET', + 'CAPTION', + 'HTML', + 'TABLE', + 'TD', + 'TH', + 'MARQUEE', + 'OBJECT', + 'TEMPLATE', + + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', + ) + ); + } + + /** + * Returns whether a particular element is in list item scope. + * + * > The stack of open elements is said to have a particular element + * > in list item scope when it has that element in the specific scope + * > consisting of the following element types: + * > + * > - All the element types listed above for the has an element in scope algorithm. + * > - ol in the HTML namespace + * > - ul in the HTML namespace + * + * @since 6.4.0 + * @since 6.5.0 Implemented: no longer throws on every invocation. + * @since 6.7.0 Supports all required HTML elements. + * + * @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether given element is in scope. + */ + public function has_element_in_list_item_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( + $tag_name, + array( + 'APPLET', + 'BUTTON', + 'CAPTION', + 'HTML', + 'TABLE', + 'TD', + 'TH', + 'MARQUEE', + 'OBJECT', + 'OL', + 'TEMPLATE', + 'UL', + + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', + ) + ); + } + + /** + * Returns whether a particular element is in button scope. + * + * > The stack of open elements is said to have a particular element + * > in button scope when it has that element in the specific scope + * > consisting of the following element types: + * > + * > - All the element types listed above for the has an element in scope algorithm. + * > - button in the HTML namespace + * + * @since 6.4.0 + * @since 6.7.0 Supports all required HTML elements. + * + * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether given element is in scope. + */ + public function has_element_in_button_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( + $tag_name, + array( + 'APPLET', + 'BUTTON', + 'CAPTION', + 'HTML', + 'TABLE', + 'TD', + 'TH', + 'MARQUEE', + 'OBJECT', + 'TEMPLATE', + + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', + ) + ); + } + + /** + * Returns whether a particular element is in table scope. + * + * > The stack of open elements is said to have a particular element + * > in table scope when it has that element in the specific scope + * > consisting of the following element types: + * > + * > - html in the HTML namespace + * > - table in the HTML namespace + * > - template in the HTML namespace + * + * @since 6.4.0 + * @since 6.7.0 Full implementation. + * + * @see https://html.spec.whatwg.org/#has-an-element-in-table-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether given element is in scope. + */ + public function has_element_in_table_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( + $tag_name, + array( + 'HTML', + 'TABLE', + 'TEMPLATE', + ) + ); + } + + /** + * Returns whether a particular element is in select scope. + * + * This test differs from the others like it, in that its rules are inverted. + * Instead of arriving at a match when one of any tag in a termination group + * is reached, this one terminates if any other tag is reached. + * + * > The stack of open elements is said to have a particular element in select scope when it has + * > that element in the specific scope consisting of all element types except the following: + * > - optgroup in the HTML namespace + * > - option in the HTML namespace + * + * @since 6.4.0 Stub implementation (throws). + * @since 6.7.0 Full implementation. + * + * @see https://html.spec.whatwg.org/#has-an-element-in-select-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether the given element is in SELECT scope. + */ + public function has_element_in_select_scope( string $tag_name ): bool { + foreach ( $this->walk_up() as $node ) { + if ( $node->node_name === $tag_name ) { + return true; + } + + if ( + 'OPTION' !== $node->node_name && + 'OPTGROUP' !== $node->node_name + ) { + return false; + } + } + + return false; + } + + /** + * Returns whether a P is in BUTTON scope. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope + * + * @return bool Whether a P is in BUTTON scope. + */ + public function has_p_in_button_scope(): bool { + return $this->has_p_in_button_scope; + } + + /** + * Pops a node off of the stack of open elements. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#stack-of-open-elements + * + * @return bool Whether a node was popped off of the stack. + */ + public function pop(): bool { + $item = array_pop( $this->stack ); + if ( null === $item ) { + return false; + } + + $this->after_element_pop( $item ); + return true; + } + + /** + * Pops nodes off of the stack of open elements until an HTML tag with the given name has been popped. + * + * @since 6.4.0 + * + * @see WP_HTML_Open_Elements::pop + * + * @param string $html_tag_name Name of tag that needs to be popped off of the stack of open elements. + * @return bool Whether a tag of the given name was found and popped off of the stack of open elements. + */ + public function pop_until( string $html_tag_name ): bool { + foreach ( $this->walk_up() as $item ) { + $this->pop(); + + if ( 'html' !== $item->namespace ) { + continue; + } + + if ( + '(internal: H1 through H6 - do not use)' === $html_tag_name && + in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) + ) { + return true; + } + + if ( $html_tag_name === $item->node_name ) { + return true; + } + } + + return false; + } + + /** + * Pushes a node onto the stack of open elements. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#stack-of-open-elements + * + * @param WP_HTML_Token $stack_item Item to add onto stack. + */ + public function push( WP_HTML_Token $stack_item ): void { + $this->stack[] = $stack_item; + $this->after_element_push( $stack_item ); + } + + /** + * Removes a specific node from the stack of open elements. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $token The node to remove from the stack of open elements. + * @return bool Whether the node was found and removed from the stack of open elements. + */ + public function remove_node( WP_HTML_Token $token ): bool { + foreach ( $this->walk_up() as $position_from_end => $item ) { + if ( $token->bookmark_name !== $item->bookmark_name ) { + continue; + } + + $position_from_start = $this->count() - $position_from_end - 1; + array_splice( $this->stack, $position_from_start, 1 ); + $this->after_element_pop( $item ); + return true; + } + + return false; + } + + + /** + * Steps through the stack of open elements, starting with the top element + * (added first) and walking downwards to the one added last. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $html = 'We are here'; + * foreach ( $stack->walk_down() as $node ) { + * echo "{$node->node_name} -> "; + * } + * > EM -> STRONG -> A -> + * + * To start with the most-recently added element and walk towards the top, + * see WP_HTML_Open_Elements::walk_up(). + * + * @since 6.4.0 + */ + public function walk_down() { + $count = count( $this->stack ); + + for ( $i = 0; $i < $count; $i++ ) { + yield $this->stack[ $i ]; + } + } + + /** + * Steps through the stack of open elements, starting with the bottom element + * (added last) and walking upwards to the one added first. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $html = 'We are here'; + * foreach ( $stack->walk_up() as $node ) { + * echo "{$node->node_name} -> "; + * } + * > A -> STRONG -> EM -> + * + * To start with the first added element and walk towards the bottom, + * see WP_HTML_Open_Elements::walk_down(). + * + * @since 6.4.0 + * @since 6.5.0 Accepts $above_this_node to start traversal above a given node, if it exists. + * + * @param WP_HTML_Token|null $above_this_node Optional. Start traversing above this node, + * if provided and if the node exists. + */ + public function walk_up( ?WP_HTML_Token $above_this_node = null ) { + $has_found_node = null === $above_this_node; + + for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) { + $node = $this->stack[ $i ]; + + if ( ! $has_found_node ) { + $has_found_node = $node === $above_this_node; + continue; + } + + yield $node; + } + } + + /* + * Internal helpers. + */ + + /** + * Updates internal flags after adding an element. + * + * Certain conditions (such as "has_p_in_button_scope") are maintained here as + * flags that are only modified when adding and removing elements. This allows + * the HTML Processor to quickly check for these conditions instead of iterating + * over the open stack elements upon each new tag it encounters. These flags, + * however, need to be maintained as items are added and removed from the stack. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $item Element that was added to the stack of open elements. + */ + public function after_element_push( WP_HTML_Token $item ): void { + $namespaced_name = 'html' === $item->namespace + ? $item->node_name + : "{$item->namespace} {$item->node_name}"; + + /* + * When adding support for new elements, expand this switch to trap + * cases where the precalculated value needs to change. + */ + switch ( $namespaced_name ) { + case 'APPLET': + case 'BUTTON': + case 'CAPTION': + case 'HTML': + case 'TABLE': + case 'TD': + case 'TH': + case 'MARQUEE': + case 'OBJECT': + case 'TEMPLATE': + case 'math MI': + case 'math MO': + case 'math MN': + case 'math MS': + case 'math MTEXT': + case 'math ANNOTATION-XML': + case 'svg FOREIGNOBJECT': + case 'svg DESC': + case 'svg TITLE': + $this->has_p_in_button_scope = false; + break; + + case 'P': + $this->has_p_in_button_scope = true; + break; + } + + if ( null !== $this->push_handler ) { + ( $this->push_handler )( $item ); + } + } + + /** + * Updates internal flags after removing an element. + * + * Certain conditions (such as "has_p_in_button_scope") are maintained here as + * flags that are only modified when adding and removing elements. This allows + * the HTML Processor to quickly check for these conditions instead of iterating + * over the open stack elements upon each new tag it encounters. These flags, + * however, need to be maintained as items are added and removed from the stack. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $item Element that was removed from the stack of open elements. + */ + public function after_element_pop( WP_HTML_Token $item ): void { + /* + * When adding support for new elements, expand this switch to trap + * cases where the precalculated value needs to change. + */ + switch ( $item->node_name ) { + case 'APPLET': + case 'BUTTON': + case 'CAPTION': + case 'HTML': + case 'P': + case 'TABLE': + case 'TD': + case 'TH': + case 'MARQUEE': + case 'OBJECT': + case 'TEMPLATE': + case 'math MI': + case 'math MO': + case 'math MN': + case 'math MS': + case 'math MTEXT': + case 'math ANNOTATION-XML': + case 'svg FOREIGNOBJECT': + case 'svg DESC': + case 'svg TITLE': + $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' ); + break; + } + + if ( null !== $this->pop_handler ) { + ( $this->pop_handler )( $item ); + } + } + + /** + * Clear the stack back to a table context. + * + * > When the steps above require the UA to clear the stack back to a table context, it means + * > that the UA must, while the current node is not a table, template, or html element, pop + * > elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-context + * + * @since 6.7.0 + */ + public function clear_to_table_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TABLE' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + + /** + * Clear the stack back to a table body context. + * + * > When the steps above require the UA to clear the stack back to a table body context, it + * > means that the UA must, while the current node is not a tbody, tfoot, thead, template, or + * > html element, pop elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-body-context + * + * @since 6.7.0 + */ + public function clear_to_table_body_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TBODY' === $item->node_name || + 'TFOOT' === $item->node_name || + 'THEAD' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + + /** + * Clear the stack back to a table row context. + * + * > When the steps above require the UA to clear the stack back to a table row context, it + * > means that the UA must, while the current node is not a tr, template, or html element, pop + * > elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-row-context + * + * @since 6.7.0 + */ + public function clear_to_table_row_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TR' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + + /** + * Wakeup magic method. + * + * @since 6.6.0 + */ + public function __wakeup() { + throw new \LogicException( __CLASS__ . ' should never be unserialized' ); + } +} diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-processor-state.php b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-processor-state.php new file mode 100644 index 00000000..b257aa80 --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-processor-state.php @@ -0,0 +1,454 @@ + + */ + public $stack_of_template_insertion_modes = array(); + + /** + * Tracks open elements while scanning HTML. + * + * This property is initialized in the constructor and never null. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#stack-of-open-elements + * + * @var WP_HTML_Open_Elements + */ + public $stack_of_open_elements; + + /** + * Tracks open formatting elements, used to handle mis-nested formatting element tags. + * + * This property is initialized in the constructor and never null. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#list-of-active-formatting-elements + * + * @var WP_HTML_Active_Formatting_Elements + */ + public $active_formatting_elements; + + /** + * Refers to the currently-matched tag, if any. + * + * @since 6.4.0 + * + * @var WP_HTML_Token|null + */ + public $current_token = null; + + /** + * Tree construction insertion mode. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#insertion-mode + * + * @var string + */ + public $insertion_mode = self::INSERTION_MODE_INITIAL; + + /** + * Context node initializing fragment parser, if created as a fragment parser. + * + * @since 6.4.0 + * @deprecated 6.8.0 WP_HTML_Processor tracks the context_node internally. + * + * @var null + */ + public $context_node = null; + + /** + * The recognized encoding of the input byte stream. + * + * > The stream of code points that comprises the input to the tokenization + * > stage will be initially seen by the user agent as a stream of bytes + * > (typically coming over the network or from the local file system). + * > The bytes encode the actual characters according to a particular character + * > encoding, which the user agent uses to decode the bytes into characters. + * + * @since 6.7.0 + * + * @var string|null + */ + public $encoding = null; + + /** + * The parser's confidence in the input encoding. + * + * > When the HTML parser is decoding an input byte stream, it uses a character + * > encoding and a confidence. The confidence is either tentative, certain, or + * > irrelevant. The encoding used, and whether the confidence in that encoding + * > is tentative or certain, is used during the parsing to determine whether to + * > change the encoding. If no encoding is necessary, e.g. because the parser is + * > operating on a Unicode stream and doesn't have to use a character encoding + * > at all, then the confidence is irrelevant. + * + * @since 6.7.0 + * + * @var string + */ + public $encoding_confidence = 'tentative'; + + /** + * HEAD element pointer. + * + * @since 6.7.0 + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#head-element-pointer + * + * @var WP_HTML_Token|null + */ + public $head_element = null; + + /** + * FORM element pointer. + * + * > points to the last form element that was opened and whose end tag has + * > not yet been seen. It is used to make form controls associate with + * > forms in the face of dramatically bad markup, for historical reasons. + * > It is ignored inside template elements. + * + * @todo This may be invalidated by a seek operation. + * + * @see https://html.spec.whatwg.org/#form-element-pointer + * + * @since 6.7.0 + * + * @var WP_HTML_Token|null + */ + public $form_element = null; + + /** + * The frameset-ok flag indicates if a `FRAMESET` element is allowed in the current state. + * + * > The frameset-ok flag is set to "ok" when the parser is created. It is set to "not ok" after certain tokens are seen. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#frameset-ok-flag + * + * @var bool + */ + public $frameset_ok = true; + + /** + * Constructor - creates a new and empty state value. + * + * @since 6.4.0 + * + * @see WP_HTML_Processor + */ + public function __construct() { + $this->stack_of_open_elements = new WP_HTML_Open_Elements(); + $this->active_formatting_elements = new WP_HTML_Active_Formatting_Elements(); + } +} diff --git a/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-processor.php b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-processor.php new file mode 100644 index 00000000..d64574e2 --- /dev/null +++ b/blueprints/symfony-package-radar/source/wordpress/symfony-package-radar/composer-packages/wordpress-html-api/src/html-api/class-wp-html-processor.php @@ -0,0 +1,6622 @@ +next_tag( array( 'breadcrumbs' => array( 'DIV', 'FIGURE', 'IMG' ) ) ) ) { + * $processor->add_class( 'responsive-image' ); + * } + * + * #### Breadcrumbs + * + * Breadcrumbs represent the stack of open elements from the root + * of the document or fragment down to the currently-matched node, + * if one is currently selected. Call WP_HTML_Processor::get_breadcrumbs() + * to inspect the breadcrumbs for a matched tag. + * + * Breadcrumbs can specify nested HTML structure and are equivalent + * to a CSS selector comprising tag names separated by the child + * combinator, such as "DIV > FIGURE > IMG". + * + * Since all elements find themselves inside a full HTML document + * when parsed, the return value from `get_breadcrumbs()` will always + * contain any implicit outermost elements. For example, when parsing + * with `create_fragment()` in the `BODY` context (the default), any + * tag in the given HTML document will contain `array( 'HTML', 'BODY', … )` + * in its breadcrumbs. + * + * Despite containing the implied outermost elements in their breadcrumbs, + * tags may be found with the shortest-matching breadcrumb query. That is, + * `array( 'IMG' )` matches all IMG elements and `array( 'P', 'IMG' )` + * matches all IMG elements directly inside a P element. To ensure that no + * partial matches erroneously match it's possible to specify in a query + * the full breadcrumb match all the way down from the root HTML element. + * + * Example: + * + * $html = '

A lovely day outside
'; + * // ----- Matches here. + * $processor->next_tag( array( 'breadcrumbs' => array( 'FIGURE', 'IMG' ) ) ); + * + * $html = '
A lovely day outside
'; + * // ---- Matches here. + * $processor->next_tag( array( 'breadcrumbs' => array( 'FIGURE', 'FIGCAPTION', 'EM' ) ) ); + * + * $html = '
'; + * // ----- Matches here, because IMG must be a direct child of the implicit BODY. + * $processor->next_tag( array( 'breadcrumbs' => array( 'BODY', 'IMG' ) ) ); + * + * ## HTML Support + * + * This class implements a small part of the HTML5 specification. + * It's designed to operate within its support and abort early whenever + * encountering circumstances it can't properly handle. This is + * the principle way in which this class remains as simple as possible + * without cutting corners and breaking compliance. + * + * ### Supported elements + * + * If any unsupported element appears in the HTML input the HTML Processor + * will abort early and stop all processing. This draconian measure ensures + * that the HTML Processor won't break any HTML it doesn't fully understand. + * + * The HTML Processor supports all elements other than a specific set: + * + * - Any element inside a TABLE. + * - Any element inside foreign content, including SVG and MATH. + * - Any element outside the IN BODY insertion mode, e.g. doctype declarations, meta, links. + * + * ### Supported markup + * + * Some kinds of non-normative HTML involve reconstruction of formatting elements and + * re-parenting of mis-nested elements. For example, a DIV tag found inside a TABLE + * may in fact belong _before_ the table in the DOM. If the HTML Processor encounters + * such a case it will stop processing. + * + * The following list illustrates some common examples of unexpected HTML inputs that + * the HTML Processor properly parses and represents: + * + * - HTML with optional tags omitted, e.g. `

one

two`. + * - HTML with unexpected tag closers, e.g. `

one more

`. + * - Non-void tags with self-closing flag, e.g. `
the DIV is still open.
`. + * - Heading elements which close open heading elements of another level, e.g. `

Closed by

`. + * - Elements containing text that looks like other tags but isn't, e.g. `The <img> is plaintext`. + * - SCRIPT and STYLE tags containing text that looks like HTML but isn't, e.g. ``. + * - SCRIPT content which has been escaped, e.g. ``. + * + * ### Unsupported Features + * + * This parser does not report parse errors. + * + * Normally, when additional HTML or BODY tags are encountered in a document, if there + * are any additional attributes on them that aren't found on the previous elements, + * the existing HTML and BODY elements adopt those missing attribute values. This + * parser does not add those additional attributes. + * + * In certain situations, elements are moved to a different part of the document in + * a process called "adoption" and "fostering." Because the nodes move to a location + * in the document that the parser had already processed, this parser does not support + * these situations and will bail. + * + * @since 6.4.0 + * + * @see WP_HTML_Tag_Processor + * @see https://html.spec.whatwg.org/ + */ +class WP_HTML_Processor extends WP_HTML_Tag_Processor { + /** + * The maximum number of bookmarks allowed to exist at any given time. + * + * HTML processing requires more bookmarks than basic tag processing, + * so this class constant from the Tag Processor is overwritten. + * + * @since 6.4.0 + * + * @var int + */ + const MAX_BOOKMARKS = 100; + + /** + * Holds the working state of the parser, including the stack of + * open elements and the stack of active formatting elements. + * + * Initialized in the constructor. + * + * @since 6.4.0 + * + * @var WP_HTML_Processor_State + */ + private $state; + + /** + * Used to create unique bookmark names. + * + * This class sets a bookmark for every tag in the HTML document that it encounters. + * The bookmark name is auto-generated and increments, starting with `1`. These are + * internal bookmarks and are automatically released when the referring WP_HTML_Token + * goes out of scope and is garbage-collected. + * + * @since 6.4.0 + * + * @see WP_HTML_Processor::$release_internal_bookmark_on_destruct + * + * @var int + */ + private $bookmark_counter = 0; + + /** + * Stores an explanation for why something failed, if it did. + * + * @see self::get_last_error + * + * @since 6.4.0 + * + * @var string|null + */ + private $last_error = null; + + /** + * Stores context for why the parser bailed on unsupported HTML, if it did. + * + * @see self::get_unsupported_exception + * + * @since 6.7.0 + * + * @var WP_HTML_Unsupported_Exception|null + */ + private $unsupported_exception = null; + + /** + * Releases a bookmark when PHP garbage-collects its wrapping WP_HTML_Token instance. + * + * This function is created inside the class constructor so that it can be passed to + * the stack of open elements and the stack of active formatting elements without + * exposing it as a public method on the class. + * + * @since 6.4.0 + * + * @var Closure|null + */ + private $release_internal_bookmark_on_destruct = null; + + /** + * Stores stack events which arise during parsing of the + * HTML document, which will then supply the "match" events. + * + * @since 6.6.0 + * + * @var WP_HTML_Stack_Event[] + */ + private $element_queue = array(); + + /** + * Stores the current breadcrumbs. + * + * @since 6.7.0 + * + * @var string[] + */ + private $breadcrumbs = array(); + + /** + * Current stack event, if set, representing a matched token. + * + * Because the parser may internally point to a place further along in a document + * than the nodes which have already been processed (some "virtual" nodes may have + * appeared while scanning the HTML document), this will point at the "current" node + * being processed. It comes from the front of the element queue. + * + * @since 6.6.0 + * + * @var WP_HTML_Stack_Event|null + */ + private $current_element = null; + + /** + * Context node if created as a fragment parser. + * + * @var WP_HTML_Token|null + */ + private $context_node = null; + + /* + * Public Interface Functions + */ + + /** + * Creates an HTML processor in the fragment parsing mode. + * + * Use this for cases where you are processing chunks of HTML that + * will be found within a bigger HTML document, such as rendered + * block output that exists within a post, `the_content` inside a + * rendered site layout. + * + * Fragment parsing occurs within a context, which is an HTML element + * that the document will eventually be placed in. It becomes important + * when special elements have different rules than others, such as inside + * a TEXTAREA or a TITLE tag where things that look like tags are text, + * or inside a SCRIPT tag where things that look like HTML syntax are JS. + * + * The context value should be a representation of the tag into which the + * HTML is found. For most cases this will be the body element. The HTML + * form is provided because a context element may have attributes that + * impact the parse, such as with a SCRIPT tag and its `type` attribute. + * + * ## Current HTML Support + * + * - The only supported context is ``, which is the default value. + * - The only supported document encoding is `UTF-8`, which is the default value. + * + * @since 6.4.0 + * @since 6.6.0 Returns `static` instead of `self` so it can create subclass instances. + * + * @param string $html Input HTML fragment to process. + * @param string $context Context element for the fragment, must be default of ``. + * @param string $encoding Text encoding of the document; must be default of 'UTF-8'. + * @return static|null The created processor if successful, otherwise null. + */ + public static function create_fragment( $html, $context = '', $encoding = 'UTF-8' ) { + if ( '' !== $context || 'UTF-8' !== $encoding ) { + return null; + } + + $context_processor = static::create_full_parser( "{$context}", $encoding ); + if ( null === $context_processor ) { + return null; + } + + while ( $context_processor->next_tag() ) { + if ( ! $context_processor->is_virtual() ) { + $context_processor->set_bookmark( 'final_node' ); + } + } + + if ( + ! $context_processor->has_bookmark( 'final_node' ) || + ! $context_processor->seek( 'final_node' ) + ) { + _doing_it_wrong( __METHOD__, __( 'No valid context element was detected.' ), '6.8.0' ); + return null; + } + + return $context_processor->create_fragment_at_current_node( $html ); + } + + /** + * Creates an HTML processor in the full parsing mode. + * + * It's likely that a fragment parser is more appropriate, unless sending an + * entire HTML document from start to finish. Consider a fragment parser with + * a context node of ``. + * + * UTF-8 is the only allowed encoding. If working with a document that + * isn't UTF-8, first convert the document to UTF-8, then pass in the + * converted HTML. + * + * @param string $html Input HTML document to process. + * @param string|null $known_definite_encoding Optional. If provided, specifies the charset used + * in the input byte stream. Currently must be UTF-8. + * @return static|null The created processor if successful, otherwise null. + */ + public static function create_full_parser( $html, $known_definite_encoding = 'UTF-8' ) { + if ( 'UTF-8' !== $known_definite_encoding ) { + return null; + } + + $processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE ); + $processor->state->encoding = $known_definite_encoding; + $processor->state->encoding_confidence = 'certain'; + + return $processor; + } + + /** + * Constructor. + * + * Do not use this method. Use the static creator methods instead. + * + * @access private + * + * @since 6.4.0 + * + * @see WP_HTML_Processor::create_fragment() + * + * @param string $html HTML to process. + * @param string|null $use_the_static_create_methods_instead This constructor should not be called manually. + */ + public function __construct( $html, $use_the_static_create_methods_instead = null ) { + parent::__construct( $html ); + + if ( self::CONSTRUCTOR_UNLOCK_CODE !== $use_the_static_create_methods_instead ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: WP_HTML_Processor::create_fragment(). */ + __( 'Call %s to create an HTML Processor instead of calling the constructor directly.' ), + 'WP_HTML_Processor::create_fragment()' + ), + '6.4.0' + ); + } + + $this->state = new WP_HTML_Processor_State(); + + $this->state->stack_of_open_elements->set_push_handler( + function ( WP_HTML_Token $token ): void { + $is_virtual = ! isset( $this->state->current_token ) || $this->is_tag_closer(); + $same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name; + $provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real'; + $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::PUSH, $provenance ); + + $this->change_parsing_namespace( $token->integration_node_type ? 'html' : $token->namespace ); + } + ); + + $this->state->stack_of_open_elements->set_pop_handler( + function ( WP_HTML_Token $token ): void { + $is_virtual = ! isset( $this->state->current_token ) || ! $this->is_tag_closer(); + $same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name; + $provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real'; + $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::POP, $provenance ); + + $adjusted_current_node = $this->get_adjusted_current_node(); + + if ( $adjusted_current_node ) { + $this->change_parsing_namespace( $adjusted_current_node->integration_node_type ? 'html' : $adjusted_current_node->namespace ); + } else { + $this->change_parsing_namespace( 'html' ); + } + } + ); + + /* + * Create this wrapper so that it's possible to pass + * a private method into WP_HTML_Token classes without + * exposing it to any public API. + */ + $this->release_internal_bookmark_on_destruct = function ( string $name ): void { + parent::release_bookmark( $name ); + }; + } + + /** + * Creates a fragment processor at the current node. + * + * HTML Fragment parsing always happens with a context node. HTML Fragment Processors can be + * instantiated with a `BODY` context node via `WP_HTML_Processor::create_fragment( $html )`. + * + * The context node may impact how a fragment of HTML is parsed. For example, consider the HTML + * fragment `Inside TD?`. + * + * A BODY context node will produce the following tree: + * + * └─#text Inside TD? + * + * Notice that the `` tags are completely ignored. + * + * Compare that with an SVG context node that produces the following tree: + * + * ├─svg:td + * └─#text Inside TD? + * + * Here, a `td` node in the `svg` namespace is created, and its self-closing flag is respected. + * This is a peculiarity of parsing HTML in foreign content like SVG. + * + * Finally, consider the tree produced with a TABLE context node: + * + * └─TBODY + * └─TR + * └─TD + * └─#text Inside TD? + * + * These examples demonstrate how important the context node may be when processing an HTML + * fragment. Special care must be taken when processing fragments that are expected to appear + * in specific contexts. SVG and TABLE are good examples, but there are others. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm + * + * @since 6.8.0 + * + * @param string $html Input HTML fragment to process. + * @return static|null The created processor if successful, otherwise null. + */ + private function create_fragment_at_current_node( string $html ) { + if ( $this->get_token_type() !== '#tag' || $this->is_tag_closer() ) { + _doing_it_wrong( + __METHOD__, + __( 'The context element must be a start tag.' ), + '6.8.0' + ); + return null; + } + + $tag_name = $this->current_element->token->node_name; + $namespace = $this->current_element->token->namespace; + + if ( 'html' === $namespace && self::is_void( $tag_name ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + // translators: %s: A tag name like INPUT or BR. + __( 'The context element cannot be a void element, found "%s".' ), + $tag_name + ), + '6.8.0' + ); + return null; + } + + /* + * Prevent creating fragments at nodes that require a special tokenizer state. + * This is unsupported by the HTML Processor. + */ + if ( + 'html' === $namespace && + in_array( $tag_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP', 'PLAINTEXT' ), true ) + ) { + _doing_it_wrong( + __METHOD__, + sprintf( + // translators: %s: A tag name like IFRAME or TEXTAREA. + __( 'The context element "%s" is not supported.' ), + $tag_name + ), + '6.8.0' + ); + return null; + } + + $fragment_processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE ); + + $fragment_processor->compat_mode = $this->compat_mode; + + // @todo Create "fake" bookmarks for non-existent but implied nodes. + $fragment_processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 ); + $root_node = new WP_HTML_Token( + 'root-node', + 'HTML', + false + ); + $fragment_processor->state->stack_of_open_elements->push( $root_node ); + + $fragment_processor->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 ); + $fragment_processor->context_node = clone $this->current_element->token; + $fragment_processor->context_node->bookmark_name = 'context-node'; + $fragment_processor->context_node->on_destroy = null; + + $fragment_processor->breadcrumbs = array( 'HTML', $fragment_processor->context_node->node_name ); + + if ( 'TEMPLATE' === $fragment_processor->context_node->node_name ) { + $fragment_processor->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE; + } + + $fragment_processor->reset_insertion_mode_appropriately(); + + /* + * > Set the parser's form element pointer to the nearest node to the context element that + * > is a form element (going straight up the ancestor chain, and including the element + * > itself, if it is a form element), if any. (If there is no such form element, the + * > form element pointer keeps its initial value, null.) + */ + foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) { + if ( 'FORM' === $element->node_name && 'html' === $element->namespace ) { + $fragment_processor->state->form_element = clone $element; + $fragment_processor->state->form_element->bookmark_name = null; + $fragment_processor->state->form_element->on_destroy = null; + break; + } + } + + $fragment_processor->state->encoding_confidence = 'irrelevant'; + + /* + * Update the parsing namespace near the end of the process. + * This is important so that any push/pop from the stack of open + * elements does not change the parsing namespace. + */ + $fragment_processor->change_parsing_namespace( + $this->current_element->token->integration_node_type ? 'html' : $namespace + ); + + return $fragment_processor; + } + + /** + * Stops the parser and terminates its execution when encountering unsupported markup. + * + * @throws WP_HTML_Unsupported_Exception Halts execution of the parser. + * + * @since 6.7.0 + * + * @param string $message Explains support is missing in order to parse the current node. + */ + private function bail( string $message ) { + $here = $this->bookmarks[ $this->state->current_token->bookmark_name ]; + $token = substr( $this->html, $here->start, $here->length ); + + $open_elements = array(); + foreach ( $this->state->stack_of_open_elements->stack as $item ) { + $open_elements[] = $item->node_name; + } + + $active_formats = array(); + foreach ( $this->state->active_formatting_elements->walk_down() as $item ) { + $active_formats[] = $item->node_name; + } + + $this->last_error = self::ERROR_UNSUPPORTED; + + $this->unsupported_exception = new WP_HTML_Unsupported_Exception( + $message, + $this->state->current_token->node_name, + $here->start, + $token, + $open_elements, + $active_formats + ); + + throw $this->unsupported_exception; + } + + /** + * Returns the last error, if any. + * + * Various situations lead to parsing failure but this class will + * return `false` in all those cases. To determine why something + * failed it's possible to request the last error. This can be + * helpful to know to distinguish whether a given tag couldn't + * be found or if content in the document caused the processor + * to give up and abort processing. + * + * Example + * + * $processor = WP_HTML_Processor::create_fragment( '