Make WordPress Core

Changeset 62687


Ignore:
Timestamp:
07/10/2026 04:45:12 PM (2 months ago)
Author:
jonsurrell
Message:

HTML API: Add HTML processing instruction support.

The HTML specification was updated to include _processing instructions_ like <?wp-processing-instruction ...data?>. Implement processing instruction support in the HTML API.

Developed in https://github.com/WordPress/wordpress-develop/pull/12424.

Props jonsurrell, dmsnell, westonruter.
Fixes #65582. See #65581.

Location:
trunk
Files:
1 added
14 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/html-api/class-wp-html-processor.php

    r62594 r62687  
    14031403                                break;
    14041404
     1405                        /**
     1406                         * Processing instructions are serialized as `"<?" target " " data "?>"`.
     1407                         *
     1408                         * @link https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments
     1409                         */
     1410                        case '#processing-instruction':
     1411                                $html .= "<?{$this->get_tag()} {$this->get_modifiable_text()}?>";
     1412                                break;
     1413
    14051414                        case '#cdata-section':
    14061415                                $html .= "<![CDATA[{$this->get_modifiable_text()}]]>";
     
    15631572                        /*
    15641573                         * > A comment token
     1574                         * > A processing instruction token
    15651575                         */
    15661576                        case '#comment':
    15671577                        case '#funky-comment':
    15681578                        case '#presumptuous-tag':
     1579                        case '#processing-instruction':
    15691580                                $this->insert_html_element( $this->state->current_token );
    15701581                                return true;
     
    16291640                        /*
    16301641                         * > A comment token
     1642                         * > A processing instruction token
    16311643                         */
    16321644                        case '#comment':
    16331645                        case '#funky-comment':
    16341646                        case '#presumptuous-tag':
     1647                        case '#processing-instruction':
    16351648                                $this->insert_html_element( $this->state->current_token );
    16361649                                return true;
     
    17341747                        /*
    17351748                         * > A comment token
     1749                         * > A processing instruction token
    17361750                         */
    17371751                        case '#comment':
    17381752                        case '#funky-comment':
    17391753                        case '#presumptuous-tag':
     1754                        case '#processing-instruction':
    17401755                                $this->insert_html_element( $this->state->current_token );
    17411756                                return true;
     
    18331848                        /*
    18341849                         * > A comment token
     1850                         * > A processing instruction token
    18351851                         */
    18361852                        case '#comment':
    18371853                        case '#funky-comment':
    18381854                        case '#presumptuous-tag':
     1855                        case '#processing-instruction':
    18391856                                $this->insert_html_element( $this->state->current_token );
    18401857                                return true;
     
    21592176                        /*
    21602177                         * > A comment token
     2178                         * > A processing instruction token
    21612179                         */
    21622180                        case '#comment':
    21632181                        case '#funky-comment':
    21642182                        case '#presumptuous-tag':
     2183                        case '#processing-instruction':
    21652184                                $this->insert_html_element( $this->state->current_token );
    21662185                                return true;
     
    23192338                        case '#funky-comment':
    23202339                        case '#presumptuous-tag':
     2340                        case '#processing-instruction':
    23212341                                $this->insert_html_element( $this->state->current_token );
    23222342                                return true;
     
    34013421                        /*
    34023422                         * > A comment token
     3423                         * > A processing instruction token
    34033424                         */
    34043425                        case '#comment':
    34053426                        case '#funky-comment':
    34063427                        case '#presumptuous-tag':
     3428                        case '#processing-instruction':
    34073429                                $this->insert_html_element( $this->state->current_token );
    34083430                                return true;
     
    37223744                        /*
    37233745                         * > A comment token
     3746                         * > A processing instruction token
    37243747                         */
    37253748                        case '#comment':
    37263749                        case '#funky-comment':
    37273750                        case '#presumptuous-tag':
     3751                        case '#processing-instruction':
    37283752                                $this->insert_html_element( $this->state->current_token );
    37293753                                return true;
     
    41544178                        /*
    41554179                         * > A comment token
     4180                         * > A processing instruction token
    41564181                         */
    41574182                        case '#comment':
    41584183                        case '#funky-comment':
    41594184                        case '#presumptuous-tag':
     4185                        case '#processing-instruction':
    41604186                                $this->insert_html_element( $this->state->current_token );
    41614187                                return true;
     
    43794405                         * > A character token
    43804406                         * > A comment token
     4407                         * > A processing instruction token
    43814408                         * > A DOCTYPE token
    43824409                         */
     
    43854412                        case '#funky-comment':
    43864413                        case '#presumptuous-tag':
     4414                        case '#processing-instruction':
    43874415                        case 'html':
    43884416                                return $this->step_in_body();
     
    45204548                        /*
    45214549                         * > A comment token
     4550                         * > A processing instruction token
    45224551                         */
    45234552                        case '#comment':
    45244553                        case '#funky-comment':
    45254554                        case '#presumptuous-tag':
     4555                        case '#processing-instruction':
    45264556                                $this->bail( 'Content outside of BODY is unsupported.' );
    45274557                                break;
     
    46134643                        /*
    46144644                         * > A comment token
     4645                         * > A processing instruction token
    46154646                         */
    46164647                        case '#comment':
    46174648                        case '#funky-comment':
    46184649                        case '#presumptuous-tag':
     4650                        case '#processing-instruction':
    46194651                                $this->insert_html_element( $this->state->current_token );
    46204652                                return true;
     
    47334765                        /*
    47344766                         * > A comment token
     4767                         * > A processing instruction token
    47354768                         */
    47364769                        case '#comment':
    47374770                        case '#funky-comment':
    47384771                        case '#presumptuous-tag':
     4772                        case '#processing-instruction':
    47394773                                $this->insert_html_element( $this->state->current_token );
    47404774                                return true;
     
    48034837                        /*
    48044838                         * > A comment token
     4839                         * > A processing instruction token
    48054840                         */
    48064841                        case '#comment':
    48074842                        case '#funky-comment':
    48084843                        case '#presumptuous-tag':
     4844                        case '#processing-instruction':
    48094845                                $this->bail( 'Content outside of HTML is unsupported.' );
    48104846                                break;
     
    48674903                        /*
    48684904                         * > A comment token
     4905                         * > A processing instruction token
    48694906                         */
    48704907                        case '#comment':
    48714908                        case '#funky-comment':
    48724909                        case '#presumptuous-tag':
     4910                        case '#processing-instruction':
    48734911                                $this->bail( 'Content outside of HTML is unsupported.' );
    48744912                                break;
     
    49905028                        /*
    49915029                         * > A comment token
     5030                         * > A processing instruction token
    49925031                         */
    49935032                        case '#comment':
    49945033                        case '#funky-comment':
    49955034                        case '#presumptuous-tag':
     5035                        case '#processing-instruction':
    49965036                                $this->insert_foreign_element( $this->state->current_token, false );
    49975037                                return true;
     
    53075347                 * > A start tag whose tag name is "image"
    53085348                 * > Change the token's tag name to "img" and reprocess it. (Don't ask.)
     5349                 *
     5350                 * This only applies to tags; a processing instruction target or a
     5351                 * comment which looks like a processing instruction may also report
     5352                 * a tag name and must not be rewritten.
    53095353                 */
    5310                 return ( 'IMAGE' === $tag_name && 'html' === $this->get_namespace() )
     5354                return ( 'IMAGE' === $tag_name && 'html' === $this->get_namespace() && '#tag' === $this->get_token_type() )
    53115355                        ? 'IMG'
    53125356                        : $tag_name;
     
    53775421         *  - `#presumptuous-tag` when matched on an empty tag closer.
    53785422         *  - `#funky-comment` when matched on a funky comment.
     5423         *  - `#processing-instruction` when matched on a processing instruction.
    53795424         *
    53805425         * @since 6.6.0 Subclassed for the HTML Processor.
     5426         * @since 7.1.0 Recognize processing instructions according to an HTML
     5427         *              specification update.
    53815428         *
    53825429         * @return string|null What kind of token is matched, or null.
  • trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php

    r62667 r62687  
    317317 *    an HTML comment when parsing. E.g. for `</%post_author>` the text is `%post_author`.
    318318 *  - `DOCTYPE` declarations like `<DOCTYPE html>` which have no closing tag.
    319  *  - XML Processing instruction nodes like `<?wp __( "Like" ); ?>` (with restrictions [2]).
     319 *  - Processing instruction nodes like `<?wp __( "Like" ); ?>` (with restrictions [2]).
    320320 *  - The empty end tag `</>` which is ignored in the browser and DOM.
    321321 *
     
    326326 *      CDATA section _were they to exist_, it will indicate this as the type of comment.
    327327 *
    328  * [2]: XML allows a broader range of characters in a processing instruction's target name
    329  *      and disallows "xml" as a name, since it's special. The Tag Processor only recognizes
    330  *      target names with an ASCII-representable subset of characters. It also exhibits the
    331  *      same constraint as with CDATA sections, in that `>` cannot exist within the token
    332  *      since Processing Instructions do not exist within HTML and their syntax transforms
    333  *      into a bogus comment in the DOM.
     328 * [2]: HTML recognizes processing instructions whose target starts with an ASCII letter
     329 *      or `_` and continues with ASCII alphanumerics, `-`, or `_`. The reserved `xml`
     330 *      and `xml-stylesheet` targets, as well as XML-valid targets with characters
     331 *      outside this set, transform into bogus comments in the DOM instead. Processing
     332 *      instructions exhibit the same constraint as CDATA sections, in that `>` cannot
     333 *      exist within the token since the processing instruction ends at the first `>`.
    334334 *
    335335 * ## Design and limitations
     
    483483         * Specifies mode of operation of the parser at any given time.
    484484         *
    485          * | State           | Meaning                                                              |
    486          * | ----------------|----------------------------------------------------------------------|
    487          * | *Ready*         | The parser is ready to run.                                          |
    488          * | *Complete*      | There is nothing left to parse.                                      |
    489          * | *Incomplete*    | The HTML ended in the middle of a token; nothing more can be parsed. |
    490          * | *Matched tag*   | Found an HTML tag; it's possible to modify its attributes.           |
    491          * | *Text node*     | Found a #text node; this is plaintext and modifiable.                |
    492          * | *CDATA node*    | Found a CDATA section; this is modifiable.                           |
    493          * | *Comment*       | Found a comment or bogus comment; this is modifiable.                |
    494          * | *Presumptuous*  | Found an empty tag closer: `</>`.                                    |
    495          * | *Funky comment* | Found a tag closer with an invalid tag name; this is modifiable.     |
     485         * | State                    | Meaning                                                              |
     486         * |--------------------------|----------------------------------------------------------------------|
     487         * | *Ready*                  | The parser is ready to run.                                          |
     488         * | *Complete*               | There is nothing left to parse.                                      |
     489         * | *Incomplete*             | The HTML ended in the middle of a token; nothing more can be parsed. |
     490         * | *Matched tag*            | Found an HTML tag; it's possible to modify its attributes.           |
     491         * | *Text node*              | Found a #text node; this is plaintext and modifiable.                |
     492         * | *CDATA node*             | Found a CDATA section; this is modifiable.                           |
     493         * | *Comment*                | Found a comment or bogus comment; this is modifiable.                |
     494         * | *Presumptuous*           | Found an empty tag closer: `</>`.                                    |
     495         * | *Funky comment*          | Found a tag closer with an invalid tag name; this is modifiable.     |
     496         * | *Processing instruction* | Found a processing instruction, e.g. `<?pi-target data>`.            |
    496497         *
    497498         * @since 6.5.0
     
    507508         * @see WP_HTML_Tag_Processor::STATE_PRESUMPTUOUS_TAG
    508509         * @see WP_HTML_Tag_Processor::STATE_FUNKY_COMMENT
     510         * @see WP_HTML_Tag_Processor::STATE_PROCESSING_INSTRUCTION
    509511         *
    510512         * @var string
     
    934936         *  - an HTML comment.
    935937         *  - a DOCTYPE declaration.
    936          *  - a processing instruction, e.g. `<?xml version="1.0" ?>`.
     938         *  - an HTML processing instruction, e.g. `<?pi …data?>`.
    937939         *
    938940         * @since 6.5.0
    939941         * @since 6.7.0 Recognizes CDATA sections within foreign content.
     942         * @since 7.1.0 Recognizes processing instructions.
    940943         *
    941944         * @return bool Whether a token was parsed.
     
    20282031                        }
    20292032
    2030                         /*
    2031                          * `<?` transitions to a bogus comment state – skip to the nearest >
    2032                          * See https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
     2033                        /**
     2034                         * `<?` transitions to the processing instruction open state.
     2035                         *
     2036                         * A processing instruction whose target starts with an ASCII letter or `_`,
     2037                         * continues with ASCII alphanumerics, `-`, or `_`, and is not an ASCII
     2038                         * case-insensitive match for `xml` or `xml-stylesheet` produces a processing
     2039                         * instruction node. Anything else transitions to the bogus comment state.
     2040                         *
     2041                         * Both forms end at the nearest `>`; a processing instruction cannot
     2042                         * contain one in the HTML syntax.
     2043                         *
     2044                         * @link https://html.spec.whatwg.org/multipage/parsing.html#processing-instruction-open-state
    20332045                         */
    20342046                        if ( ! $this->is_closing_tag && '?' === $html[ $at + 1 ] ) {
     
    20382050
    20392051                                        return false;
     2052                                }
     2053
     2054                                $target_at     = $at + 2;
     2055                                $target_length = 0;
     2056                                $first_char    = $html[ $target_at ];
     2057                                if (
     2058                                        ( 'a' <= $first_char && 'z' >= $first_char ) ||
     2059                                        ( 'A' <= $first_char && 'Z' >= $first_char ) ||
     2060                                        '_' === $first_char
     2061                                ) {
     2062                                        $target_length = 1 + strspn( $html, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-', $target_at + 1 );
     2063                                }
     2064
     2065                                /*
     2066                                 * In the processing instruction target state, only whitespace, `?`,
     2067                                 * or `>` may terminate the target; any other character converts the
     2068                                 * token into a bogus comment.
     2069                                 *
     2070                                 * The `xml` and `xml-stylesheet` targets are reserved and disallowed;
     2071                                 * they also convert the token into a bogus comment.
     2072                                 */
     2073                                $is_valid_pi = (
     2074                                        0 !== $target_length &&
     2075                                        false !== strpos( " \t\f\r\n?>", $html[ $target_at + $target_length ] ) &&
     2076                                        ! ( 3 === $target_length && 0 === substr_compare( $html, 'xml', $target_at, 3, true ) ) &&
     2077                                        ! ( 14 === $target_length && 0 === substr_compare( $html, 'xml-stylesheet', $target_at, 14, true ) )
     2078                                );
     2079
     2080                                if ( $is_valid_pi ) {
     2081                                        /*
     2082                                         * The processing instruction data starts after any whitespace
     2083                                         * following the target and ends at the `>`. When the token is
     2084                                         * closed by `?>`, that final `?` is not part of the data.
     2085                                         */
     2086                                        $data_at  = $target_at + $target_length;
     2087                                        $data_at += strspn( $html, " \t\f\r\n", $data_at );
     2088
     2089                                        $data_length = $closer_at - $data_at;
     2090                                        if ( $data_length > 0 && '?' === $html[ $closer_at - 1 ] ) {
     2091                                                --$data_length;
     2092                                        }
     2093
     2094                                        $this->parser_state         = self::STATE_PROCESSING_INSTRUCTION;
     2095                                        $this->tag_name_starts_at   = $target_at;
     2096                                        $this->tag_name_length      = $target_length;
     2097                                        $this->token_length         = $closer_at + 1 - $this->token_starts_at;
     2098                                        $this->text_starts_at       = $data_at;
     2099                                        $this->text_length          = $data_length;
     2100                                        $this->bytes_already_parsed = $closer_at + 1;
     2101                                        return true;
    20402102                                }
    20412103
     
    20482110
    20492111                                /*
    2050                                  * Identify a Processing Instruction node were HTML to have them.
     2112                                 * Identify an XML-like Processing Instruction node.
    20512113                                 *
    2052                                  * This section must occur after identifying the bogus comment end
    2053                                  * because in an HTML parser it will span to the nearest `>`, even
    2054                                  * if there's no `?>` as would be required in an XML document. It
    2055                                  * is therefore not possible to parse a Processing Instruction node
    2056                                  * containing a `>` in the HTML syntax.
    2057                                  *
    2058                                  * XML allows for more target names, but this code only identifies
    2059                                  * those with ASCII-representable target names. This means that it
    2060                                  * may identify some Processing Instruction nodes as bogus comments,
    2061                                  * but it will not misinterpret the HTML structure. By limiting the
    2062                                  * identification to these target names the Tag Processor can avoid
    2063                                  * the need to start parsing UTF-8 sequences.
     2114                                 * HTML and XML processing instructions have different parsing rules.
     2115                                 * The HTML API recognizes XML-like processing instructions that are
     2116                                 * _not_ HTML processing instructions. The HTML standard transforms
     2117                                 * them to "bogus comments," represented by the HTML API as comments
     2118                                 * with the `COMMENT_AS_PI_NODE_LOOKALIKE` type. This includes the
     2119                                 * special targets `xml` and `xml-stylesheet` which are reserved
     2120                                 * targets not allowed in HTML processing instructions.
    20642121                                 *
    20652122                                 * > NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] |
     
    20682125                                 *                     [#x10000-#xEFFFF]
    20692126                                 * > NameChar      ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
    2070                                  *
    2071                                  * @todo Processing instruction nodes in SGML may contain any kind of markup. XML defines a
    2072                                  *       special case with `<?xml ... ?>` syntax, but the `?` is part of the bogus comment.
    20732127                                 *
    20742128                                 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-PITarget
     
    29623016                }
    29633017
     3018                /*
     3019                 * Processing instruction targets are case-sensitive
     3020                 * and returned as they appear in the input HTML.
     3021                 */
     3022                if ( self::STATE_PROCESSING_INSTRUCTION === $this->parser_state ) {
     3023                        return $tag_name;
     3024                }
     3025
    29643026                if (
    29653027                        self::STATE_COMMENT === $this->parser_state &&
     
    34383500         *  - `#presumptuous-tag` when matched on an empty tag closer.
    34393501         *  - `#funky-comment` when matched on a funky comment.
     3502         *  - `#processing-instruction` when matched on a processing instruction.
    34403503         *
    34413504         * @since 6.5.0
     3505         * @since 7.1.0 Recognizes processing instructions.
    34423506         *
    34433507         * @return string|null What kind of token is matched, or null.
     3508         * @phpstan-return '#tag'|'#text'|'#cdata-section'|'#comment'|'#doctype'|'#presumptuous-tag'|'#funky-comment'|'#processing-instruction'|null
    34443509         */
    34453510        public function get_token_type(): ?string {
     
    34983563                        case self::STATE_FUNKY_COMMENT:
    34993564                                return '#funky-comment';
     3565
     3566                        case self::STATE_PROCESSING_INSTRUCTION:
     3567                                return '#processing-instruction';
    35003568                }
    35013569
     
    35383606         * This differs from {@see ::get_modifiable_text()} in that certain comment
    35393607         * types in the HTML API cannot allow their entire comment text content to
    3540          * be modified. Namely, "bogus comments" of the form `<?not allowed in html>`
     3608         * be modified. Namely, "bogus comments" of the form `<?xml not allowed in html>`
    35413609         * will create a comment whose text content starts with `?`. Note that if
    35423610         * that character were modified, it would be possible to change the node
     
    37063774                        ? $this->lexical_updates['modifiable text']->text
    37073775                        : substr( $this->html, $this->text_starts_at, $this->text_length );
     3776
     3777                /*
     3778                 * An enqueued processing instruction update holds normalized raw
     3779                 * syntax spanning from the end of the target through the end of
     3780                 * the token: a separating space, the data, and the `?>` closer.
     3781                 * The data is found by skipping the leading whitespace and
     3782                 * dropping the two bytes of the closer.
     3783                 *
     3784                 * @see WP_HTML_Tag_Processor::set_modifiable_text()
     3785                 */
     3786                if ( $has_enqueued_update && self::STATE_PROCESSING_INSTRUCTION === $this->parser_state ) {
     3787                        $text = substr( $text, strspn( $text, " \t\f\r\n" ), -2 );
     3788                }
    37083789
    37093790                /*
     
    37203801                $text = str_replace( "\r", "\n", $text );
    37213802
    3722                 // Comment data is not decoded.
     3803                // Comment and processing instruction data is not decoded.
    37233804                if (
    37243805                        self::STATE_CDATA_NODE === $this->parser_state ||
    37253806                        self::STATE_COMMENT === $this->parser_state ||
    37263807                        self::STATE_DOCTYPE === $this->parser_state ||
    3727                         self::STATE_FUNKY_COMMENT === $this->parser_state
     3808                        self::STATE_FUNKY_COMMENT === $this->parser_state ||
     3809                        self::STATE_PROCESSING_INSTRUCTION === $this->parser_state
    37283810                ) {
    37293811                        return str_replace( "\x00", "\u{FFFD}", $text );
     
    37943876         * cases, updates will be rejected and it’s up to calling code to perform
    37953877         * language-specific escaping or workarounds. Similarly, it will not allow
    3796          * setting content into a comment which would prematurely terminate the comment.
     3878         * setting content into a comment which would prematurely terminate the comment,
     3879         * or processing instruction data which cannot be represented: data containing
     3880         * a `>`, which would prematurely terminate the processing instruction, or data
     3881         * with leading whitespace, which is indistinguishable from the whitespace
     3882         * separating the data from its target.
    37973883         *
    37983884         * Example:
     
    38323918         * @since 6.7.0
    38333919         * @since 6.9.0 Escapes all character references instead of trying to avoid double-escaping.
     3920         * @since 7.1.0 Supports setting processing instruction data.
    38343921         *
    38353922         * @param string $plaintext_content New text content to represent in the matched token.
     
    38633950                        // Check if the text could close the comment.
    38643951                        if ( 1 === preg_match( '/--!?>/', $plaintext_content ) ) {
     3952                                _doing_it_wrong(
     3953                                        __METHOD__,
     3954                                        __( 'Comment text cannot contain a comment closer.' ),
     3955                                        '7.1.0'
     3956                                );
    38653957                                return false;
    38663958                        }
     
    38703962                                $this->text_length,
    38713963                                $plaintext_content
     3964                        );
     3965
     3966                        return true;
     3967                }
     3968
     3969                // Processing instruction data is not encoded.
     3970                if ( self::STATE_PROCESSING_INSTRUCTION === $this->parser_state ) {
     3971                        /*
     3972                         * A processing instruction ends at the first `>` in its
     3973                         * raw syntax: data containing one cannot be represented.
     3974                         */
     3975                        if ( str_contains( $plaintext_content, '>' ) ) {
     3976                                _doing_it_wrong(
     3977                                        __METHOD__,
     3978                                        __( 'Processing instruction data cannot contain ">".' ),
     3979                                        '7.1.0'
     3980                                );
     3981                                return false;
     3982                        }
     3983
     3984                        /*
     3985                         * All whitespace between the target and the data is skipped when
     3986                         * parsing: data with leading whitespace cannot be represented.
     3987                         */
     3988                        if ( 0 !== strspn( $plaintext_content, " \t\f\r\n" ) ) {
     3989                                _doing_it_wrong(
     3990                                        __METHOD__,
     3991                                        __( 'Processing instruction data cannot start with whitespace. Try ltrim( $plaintext_content, " \t\f\r\n" ).' ),
     3992                                        '7.1.0'
     3993                                );
     3994                                return false;
     3995                        }
     3996
     3997                        /**
     3998                         * A single replacement spans from the end of the target through
     3999                         * the end of the token, normalizing the raw syntax for that
     4000                         * region into a fixed form: a separating space, the data, and
     4001                         * the `?>` closer.
     4002                         *
     4003                         * {@see self::get_modifiable_text()} performs necessary parsing to
     4004                         * return the correct processing instruction data based
     4005                         * on the modifiable text lexical update.
     4006                         */
     4007                        $data_at = $this->tag_name_starts_at + $this->tag_name_length;
     4008
     4009                        $this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement(
     4010                                $data_at,
     4011                                $this->token_starts_at + $this->token_length - $data_at,
     4012                                " {$plaintext_content}?>"
    38724013                        );
    38734014
     
    38834024                        'html' !== $this->get_namespace()
    38844025                ) {
     4026                        _doing_it_wrong(
     4027                                __METHOD__,
     4028                                __( 'This token does not support setting modifiable text.' ),
     4029                                '7.1.0'
     4030                        );
    38854031                        return false;
    38864032                }
     
    39164062                                        false !== stripos( $plaintext_content, '</script' )
    39174063                                ) {
     4064                                        _doing_it_wrong(
     4065                                                __METHOD__,
     4066                                                __( 'SCRIPT text with an unrecognized content type cannot contain a SCRIPT tag. Apply the escaping appropriate for the content type.' ),
     4067                                                '7.1.0'
     4068                                        );
    39184069                                        return false;
    39194070                                }
     
    39804131                }
    39814132
     4133                _doing_it_wrong(
     4134                        __METHOD__,
     4135                        __( 'Only the SCRIPT, STYLE, TEXTAREA, and TITLE tags support setting modifiable text.' ),
     4136                        '7.1.0'
     4137                );
    39824138                return false;
    39834139        }
     
    49945150
    49955151        /**
     5152         * Indicates that the parser has found a processing instruction
     5153         * and it's possible to read its target and data.
     5154         *
     5155         * Example:
     5156         *
     5157         *     <?wp-bit {"just": "kidding"}>
     5158         *
     5159         * Processing instructions with an allowable target are parsed
     5160         * into processing instruction nodes. The reserved `xml` and
     5161         * `xml-stylesheet` targets, and targets with characters outside
     5162         * an ASCII-representable subset, are turned into bogus comments.
     5163         *
     5164         * @link https://html.spec.whatwg.org/multipage/parsing.html#processing-instruction-open-state
     5165         *
     5166         * @since 7.1.0
     5167         *
     5168         * @access private
     5169         */
     5170        const STATE_PROCESSING_INSTRUCTION = 'STATE_PROCESSING_INSTRUCTION';
     5171
     5172        /**
    49965173         * Indicates that a comment was created when encountering abruptly-closed HTML comment.
    49975174         *
     
    50335210        /**
    50345211         * Indicates that a comment would be parsed as a Processing
    5035          * Instruction node, were they to exist within HTML.
     5212         * Instruction node, were its target allowed within HTML.
    50365213         *
    50375214         * Example:
    50385215         *
    5039          *     <?wp __( 'Like' ) ?>
    5040          *
    5041          * This is an HTML comment, but it looks like a CDATA node.
     5216         *     <?xml version="1.0" ?>
     5217         *     <?wp.like count=5 ?>
     5218         *
     5219         * These are HTML comments, but they look like processing
     5220         * instructions. HTML parses processing instructions with
     5221         * an allowable target into processing instruction nodes,
     5222         * but the reserved `xml` and `xml-stylesheet` targets and
     5223         * XML-valid targets with characters outside of the allowed
     5224         * set become bogus comments instead.
    50425225         *
    50435226         * @since 6.5.0
     5227         * @since 7.1.0 Only applies to reserved and XML-specific target names;
     5228         *              other processing instructions produce their own token.
    50445229         */
    50455230        const COMMENT_AS_PI_NODE_LOOKALIKE = 'COMMENT_AS_PI_NODE_LOOKALIKE';
     
    50515236         * Example:
    50525237         *
    5053          *     <?nothing special>
     5238         *     <?= nothing special ?>
    50545239         *     <!{nothing special}>
    50555240         *
  • trunk/src/wp-includes/script-loader.php

    r62652 r62687  
    30493049
    30503050        if ( ! $processor->set_modifiable_text( $data ) ) {
    3051                 _doing_it_wrong(
    3052                         __FUNCTION__,
    3053                         __( 'Unable to set inline script data.' ),
    3054                         '7.0.0'
    3055                 );
    30563051                return '';
    30573052        }
  • trunk/tests/phpunit/includes/build-visual-html-tree.php

    r62655 r62687  
    211211                                break;
    212212
     213                        case '#processing-instruction':
     214                                // Processing instructions must be "<?", the target, a space, the data, "?", and ">".
     215                                $output .= str_repeat( $tree_indent, $indent_level ) . "<?{$processor->get_tag()} {$processor->get_modifiable_text()}?>\n";
     216                                break;
     217
    213218                        case '#funky-comment':
    214219                                // Comments must be "<" then "!-- " then the data then " -->".
  • trunk/tests/phpunit/tests/build-visual-html-tree.php

    r61519 r62687  
    7474
    7575                yield 'Text nodes in blocks' => array( $block_markup, $tree_structure );
    76         }
    77 
    78         /**
     76
     77                yield 'Processing instruction' => array(
     78                        '<div>before<?target arbitrary="data & things" >after',
     79                        <<<'TREE'
     80                        <div>
     81                          "before"
     82                          <?target arbitrary="data & things" ?>
     83                          "after"
     84
     85                        TREE,
     86                );
     87        }
     88
     89        /**
     90         * @ticket 65582
    7991         * @ticket 63527
    8092         * @ticket 64531
  • trunk/tests/phpunit/tests/dependencies/wpInlineScriptTag.php

    r61485 r62687  
    170170         *
    171171         * @ticket 64500
     172         *
     173         * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_modifiable_text
    172174         */
    173175        public function test_script_tag_dangerous_unescapeable_contents() {
    174                 $this->setExpectedIncorrectUsage( 'wp_get_inline_script_tag' );
    175176                /*
    176177                 * </script> cannot be printed inside a script tag
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php

    r62574 r62687  
    383383                        'Immediately-closed markup instruction' => array( '<!', '?', '>' ),
    384384                        'Warning Symbol'                        => array( '<!', '', '>' ),
    385                         'PHP block look-alike'                  => array( '<', '?php foo(); ?', '>' ),
     385                        'PHP short echo tag'                    => array( '<', '?= "Hello" ?', '>' ),
    386386                        'Funky comment'                         => array( '</', '%display-name', '>' ),
    387387                        'XML Processing Instruction look-alike' => array( '<', '?xml foo ', '>' ),
     388                );
     389        }
     390
     391        /**
     392         * Ensures that processing instructions are serialized in their normative form.
     393         *
     394         * Note that the serialized form separates the target from the data with a
     395         * single space and always terminates with `?>`, regardless of the original
     396         * syntax. The closer's `?` is dropped on parse, so this form represents any
     397         * data, including data ending in `?`.
     398         *
     399         * @ticket 61530
     400         *
     401         * @dataProvider data_processing_instructions
     402         *
     403         * @param string $html     Input containing a processing instruction.
     404         * @param string $expected Normative serialization of the input.
     405         */
     406        public function test_serializes_processing_instructions( string $html, string $expected ): void {
     407                $this->assertSame(
     408                        WP_HTML_Processor::normalize( $html ),
     409                        $expected,
     410                        'Should have serialized the processing instruction in its normative form.'
     411                );
     412        }
     413
     414        /**
     415         * Ensures that normalizing an already-normalized processing instruction does not change it.
     416         *
     417         * @ticket 61530
     418         *
     419         * @dataProvider data_processing_instructions
     420         *
     421         * @param string $html     Input containing a processing instruction.
     422         * @param string $expected Normative serialization of the input.
     423         */
     424        public function test_processing_instruction_normalization_is_idempotent( string $html, string $expected ): void {
     425                $this->assertSame(
     426                        $expected,
     427                        WP_HTML_Processor::normalize( $expected ),
     428                        'Normalizing an already-normalized processing instruction should not change it.'
     429                );
     430        }
     431
     432        /**
     433         * Data provider.
     434         *
     435         * @return array<string, array{0: string, 1: string}>
     436         */
     437        public function data_processing_instructions(): array {
     438                return array(
     439                        'PHP block'                     => array( '<?php foo(); ?>', '<?php foo(); ?>' ),
     440                        'Unclosed PHP block'            => array( '<?php foo(); >', '<?php foo(); ?>' ),
     441                        'Empty data'                    => array( '<?wp-bit?>', '<?wp-bit ?>' ),
     442                        'Whitespace-only data'          => array( '<?wp-bit   ?>', '<?wp-bit ?>' ),
     443                        'Data ending in question mark'  => array( '<?target data??>', '<?target data??>' ),
     444                        'Data of a lone question mark'  => array( '<?wp-bit ??>', '<?wp-bit ??>' ),
     445                        'Data with question mark runs'  => array( '<?wp-bit what?? news??>', '<?wp-bit what?? news??>' ),
     446                        'Question mark then whitespace' => array( '<?wp-bit sure? >', '<?wp-bit sure? ?>' ),
    388447                );
    389448        }
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php

    r62655 r62687  
    304304         * Data provider.
    305305         *
    306          * @return array[]
    307          */
    308         public static function data_self_contained_node_tokens() {
     306         * @return array<string, array{string}>
     307         */
     308        public static function data_self_contained_node_tokens(): array {
    309309                $self_contained_nodes = array(
    310                         'Normative comment'                => array( '<!-- comment -->' ),
    311                         'Comment with invalid closing'     => array( '<!-- comment --!>' ),
    312                         'CDATA Section lookalike'          => array( '<![CDATA[ comment ]]>' ),
    313                         'Processing Instruction lookalike' => array( '<?ok comment ?>' ),
    314                         'Funky comment'                    => array( '<//wp:post-meta key=isbn>' ),
    315                         'Text node'                        => array( 'Trombone' ),
     310                        'Normative comment'              => array( '<!-- comment -->' ),
     311                        'Comment with invalid closing'   => array( '<!-- comment --!>' ),
     312                        'CDATA Section lookalike'        => array( '<![CDATA[ comment ]]>' ),
     313                        'Processing Instruction'         => array( '<?ok pi ?>' ),
     314                        'Bogus PI-lookalike xml comment' => array( '<?xml version="1.0"?>' ),
     315                        'Bogus comment'                  => array( '<?🔥?>' ),
     316                        'Funky comment'                  => array( '<//wp:post-meta key=isbn>' ),
     317                        'Text node'                      => array( 'Trombone' ),
    316318                );
    317319
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorComments.php

    r62507 r62687  
    3333         * Data provider.
    3434         *
    35          * @return array[]
     35         * @return array<string, array{0:string, 1:string, 2:string, 3?:string}>
    3636         */
    37         public static function data_comments() {
     37        public static function data_comments(): array {
    3838                return array(
    39                         'Normative comment'              => array( '<!-- A comment. -->', WP_HTML_Processor::COMMENT_AS_HTML_COMMENT, ' A comment. ' ),
    40                         'Abruptly closed comment'        => array( '<!-->', WP_HTML_Processor::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT, '' ),
    41                         'Invalid HTML comment !'         => array( '<! Bang opener >', WP_HTML_Processor::COMMENT_AS_INVALID_HTML, ' Bang opener ' ),
    42                         'Invalid HTML comment ?'         => array( '<? Question opener >', WP_HTML_Processor::COMMENT_AS_INVALID_HTML, ' Question opener ' ),
    43                         'CDATA comment'                  => array( '<![CDATA[ cdata body ]]>', WP_HTML_Processor::COMMENT_AS_CDATA_LOOKALIKE, ' cdata body ' ),
    44                         'Processing instruction comment' => array( '<?pi-target Instruction body. ?>', WP_HTML_Processor::COMMENT_AS_PI_NODE_LOOKALIKE, ' Instruction body. ', 'pi-target' ),
    45                         'Processing instruction php'     => array( '<?php const HTML_COMMENT = true; ?>', WP_HTML_Processor::COMMENT_AS_PI_NODE_LOOKALIKE, ' const HTML_COMMENT = true; ', 'php' ),
     39                        'Normative comment'                       => array( '<!-- A comment. -->', WP_HTML_Processor::COMMENT_AS_HTML_COMMENT, ' A comment. ' ),
     40                        'Abruptly closed comment'                 => array( '<!-->', WP_HTML_Processor::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT, '' ),
     41                        'Invalid HTML comment !'                  => array( '<! Bang opener >', WP_HTML_Processor::COMMENT_AS_INVALID_HTML, ' Bang opener ' ),
     42                        'Invalid HTML comment ?'                  => array( '<? Question opener >', WP_HTML_Processor::COMMENT_AS_INVALID_HTML, ' Question opener ' ),
     43                        'CDATA comment'                           => array( '<![CDATA[ cdata body ]]>', WP_HTML_Processor::COMMENT_AS_CDATA_LOOKALIKE, ' cdata body ' ),
     44                        'Processing instruction xml'              => array( '<?xml version="1.0" ?>', WP_HTML_Processor::COMMENT_AS_PI_NODE_LOOKALIKE, ' version="1.0" ', 'xml' ),
     45                        'Processing instruction xml-stylesheet'   => array( '<?xml-stylesheet href="a.css" ?>', WP_HTML_Processor::COMMENT_AS_PI_NODE_LOOKALIKE, ' href="a.css" ', 'xml-stylesheet' ),
     46                        'Processing instruction XML-valid target' => array( '<?wp.bit const HTML_COMMENT = true; ?>', WP_HTML_Processor::COMMENT_AS_PI_NODE_LOOKALIKE, ' const HTML_COMMENT = true; ', 'wp.bit' ),
     47                        'Processing instruction bad target'       => array( '<?$var Not a target. ?>', WP_HTML_Processor::COMMENT_AS_INVALID_HTML, '$var Not a target. ?' ),
     48                        'PHP short echo tag'                      => array( '<?= "Hello" ?>', WP_HTML_Processor::COMMENT_AS_INVALID_HTML, '= "Hello" ?' ),
    4649                );
    4750        }
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorModifiableText.php

    r61796 r62687  
    7878         * @ticket 64751
    7979         * @dataProvider data_set_modifiable_fails_non_atomic_tags
     80         *
     81         * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_modifiable_text
    8082         */
    8183        public function test_set_modifiable_fails_non_atomic_tags(
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorWebPlatformTests.php

    r62655 r62687  
    3838                'tests2/line0697'           => 'Unimplemented: This parser does not add missing attributes to existing HTML or BODY tags.',
    3939                'tests2/line0709'           => 'Unimplemented: This parser does not add missing attributes to existing HTML or BODY tags.',
    40                 'tests1/line0601'           => 'Unimplemented: This parser treats processing instructions as comments.',
    41                 'tests1/line0640'           => 'Unimplemented: This parser treats processing instructions as comments.',
    42                 'html5test-com/line0129'    => 'Unimplemented: This parser treats processing instructions as comments.',
    4340                'menuitem-element/line0161' => 'Unimplemented: This parser does not support customizable SELECT element content.',
    4441                'tests9/line0048'           => 'Unimplemented: This parser does not support customizable SELECT element content.',
     
    6158                'webkit02/line0732'         => 'Unimplemented: This parser does not support customizable SELECT element content.',
    6259                'webkit02/line0748'         => 'Unimplemented: This parser does not support customizable SELECT element content.',
    63 
    64                 'tests1/line0602'           => 'Unimplemented: Updated Processing Instruction parsing.',
    65                 'tests1/line0641'           => 'Unimplemented: Updated Processing Instruction parsing.',
    66         );
    67 
    68         /**
    69          * Skip test files that exercise parser behavior unsupported by the HTML API.
    70          */
    71         const SKIP_TEST_PREFIXES = array(
    72                 'processing-instructions/' => 'Unimplemented: Updated Processing Instruction parsing.',
    7360        );
    7461
     
    177164                if ( array_key_exists( $test_name, self::SKIP_TESTS ) ) {
    178165                        return true;
    179                 }
    180 
    181                 foreach ( array_keys( self::SKIP_TEST_PREFIXES ) as $test_prefix ) {
    182                         if ( str_starts_with( $test_name, $test_prefix ) ) {
    183                                 return true;
    184                         }
    185166                }
    186167
     
    345326                                        break;
    346327
     328                                case '#processing-instruction':
     329                                        /*
     330                                         * Processing instructions must be "<?" then the target then,
     331                                         * unless the data is empty, a space and the data, and finally "?>".
     332                                         */
     333                                        $pi_data = $processor->get_modifiable_text();
     334                                        $output .= str_repeat( self::TREE_INDENT, $indent_level ) . "<?{$processor->get_tag()} {$pi_data}?>\n";
     335                                        break;
     336
    347337                                case '#comment':
    348338                                        // Comments must be "<" then "!-- " then the data then " -->".
     
    475465                                 * must start with "| ", followed by two spaces per parent node that the node has before
    476466                                 * the root document node.
    477                                  *
    478                                  * - Element nodes must be represented by a "<" then the tag name string ">", and all the attributes must be given, sorted lexicographically by UTF-16 code unit according to their attribute name string, on subsequent lines, as if they were children of the element node.
    479                                  * - Attribute nodes must have the attribute name string, then an "=" sign, then the attribute value in double quotes (").
    480                                  * - Text nodes must be the string, in double quotes. Newlines aren't escaped.
    481                                  * - Comments must be "<" then "!-- " then the data then " -->".
    482                                  * - DOCTYPEs must be "<!DOCTYPE " then the name then if either of the system id or public id is non-empty a space, public id in double-quotes, another space an the system id in double-quotes, and then in any case ">".
    483                                  * - Processing instructions must be "<?", then the target, then a space, then the data and then ">". (The HTML parser cannot emit processing instructions, but scripts can, and the WebVTT to DOM rules can emit them.)
    484                                  * - Template contents are represented by the string "content" with the children below it.
    485467                                 */
    486468                                case 'document':
  • trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php

    r62439 r62687  
    580580         *
    581581         * @since 6.5.0
     582         * @since 7.1.0 Processing instructions produce their own token type.
    582583         *
    583584         * @covers WP_HTML_Tag_Processor::next_token
     
    585586        public function test_basic_assertion_processing_instruction() {
    586587                $processor = new WP_HTML_Tag_Processor( '<?wp-bit {"just": "kidding"}?>' );
     588                $processor->next_token();
     589
     590                $this->assertSame(
     591                        '#processing-instruction',
     592                        $processor->get_token_name(),
     593                        "Should have found processing instruction token but found {$processor->get_token_name()} instead."
     594                );
     595
     596                $this->assertNull(
     597                        $processor->get_comment_type(),
     598                        'Should not have detected a comment type on a non-comment token.'
     599                );
     600
     601                $this->assertSame(
     602                        'wp-bit',
     603                        $processor->get_tag(),
     604                        "Should have found PI target as tag name but found {$processor->get_tag()} instead."
     605                );
     606
     607                $this->assertNull(
     608                        $processor->get_attribute( 'type' ),
     609                        'Should not have been able to query attributes on non-element token.'
     610                );
     611
     612                $this->assertSame(
     613                        '{"just": "kidding"}',
     614                        $processor->get_modifiable_text(),
     615                        'Found incorrect modifiable text.'
     616                );
     617        }
     618
     619        /**
     620         * Ensures that Processing Instruction nodes with reserved XML targets
     621         * are properly parsed as bogus comments.
     622         *
     623         * @ticket 60170
     624         *
     625         * @since 7.1.0
     626         *
     627         * @covers WP_HTML_Tag_Processor::next_token
     628         */
     629        public function test_basic_assertion_xml_processing_instruction(): void {
     630                $processor = new WP_HTML_Tag_Processor( '<?xml version="1.0"?>' );
    587631                $processor->next_token();
    588632
     
    600644
    601645                $this->assertSame(
    602                         'wp-bit',
     646                        'xml',
    603647                        $processor->get_tag(),
    604648                        "Should have found PI target as tag name but found {$processor->get_tag()} instead."
     
    606650
    607651                $this->assertNull(
    608                         $processor->get_attribute( 'type' ),
     652                        $processor->get_attribute( 'version' ),
    609653                        'Should not have been able to query attributes on non-element token.'
    610654                );
    611655
    612656                $this->assertSame(
    613                         ' {"just": "kidding"}',
    614                         $processor->get_modifiable_text(),
    615                         'Found incorrect modifiable text.'
     657                        ' version="1.0"',
     658                        $processor->get_modifiable_text(),
     659                        'Found incorrect modifiable text.'
     660                );
     661
     662                $this->assertSame(
     663                        '?xml version="1.0"?',
     664                        $processor->get_full_comment_text(),
     665                        'Found incorrect full comment text.'
    616666                );
    617667        }
  • trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php

    r62598 r62687  
    658658                        'Empty comment'                 => array( '<!---->', 1, '<!---->' ),
    659659                        'Funky comment'                 => array( '</_ funk >', 1, '</_ funk >' ),
    660                         'PI lookalike comment'          => array( '<?processing instruction?>', 1, '<?processing instruction?>' ),
     660                        'Processing instruction'        => array( '<?processing instruction?>', 1, '<?processing instruction?>' ),
     661                        'PI lookalike comment'          => array( '<?xml version="1.0"?>', 1, '<?xml version="1.0"?>' ),
    661662                        'CDATA lookalike comment'       => array( '<![CDATA[ see? data ]]>', 1, '<![CDATA[ see? data ]]>' ),
    662663                );
  • trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php

    r62507 r62687  
    350350         * @dataProvider data_tokens_not_supporting_modifiable_text_updates
    351351         *
     352         * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_modifiable_text
     353         *
    352354         * @param string $html             Contains HTML with a token not supporting modifiable text updates.
    353355         * @param int    $advance_n_tokens Count of times to run `next_token()` before reaching target node.
     
    429431                        'Text node (start)'       => array( 'Text', 1, 'Blubber', 'Blubber' ),
    430432                        'Text node (middle)'      => array( '<em>Bold move</em>', 2, 'yo', '<em>yo</em>' ),
     433                        'PI node'                 => array( 'before<?wp-bit data?>after', 2, 'other', 'before<?wp-bit other?>after' ),
     434                        'PI node (no separator)'  => array( '<?wp-bit?>', 1, '{"just": "kidding"}', '<?wp-bit {"just": "kidding"}?>' ),
    431435                        'Text node (end)'         => array( '<img>of a dog', 2, 'of a cat', '<img>of a cat' ),
    432436                        'Encoded text node'       => array( '<figcaption>birds and dogs</figcaption>', 2, '<birds> & <dogs>', '<figcaption>&lt;birds&gt; &amp; &lt;dogs&gt;</figcaption>' ),
     
    443447
    444448        /**
     449         * Ensures that processing instruction data updates re-parse to the value which was set.
     450         *
     451         * The processing instruction syntax cannot represent every data value verbatim:
     452         * a separating space must be added when the data would abut the target, and
     453         * the update always writes the `?>` form of the closer, whose `?` is dropped
     454         * when parsing, so that data ending in `?` remains intact.
     455         *
     456         * @ticket 61530
     457         *
     458         * @dataProvider data_processing_instruction_data_updates
     459         *
     460         * @param string $html     Contains a processing instruction as its first token.
     461         * @param string $new_data Data to set on the processing instruction.
     462         * @param string $expected Expected document after the update.
     463         */
     464        public function test_sets_processing_instruction_data( string $html, string $new_data, string $expected ): void {
     465                $processor = new WP_HTML_Tag_Processor( $html );
     466                $processor->next_token();
     467
     468                $this->assertSame(
     469                        '#processing-instruction',
     470                        $processor->get_token_name(),
     471                        'Should have found a processing instruction: check test setup.'
     472                );
     473
     474                $target = $processor->get_tag();
     475
     476                $this->assertTrue(
     477                        $processor->set_modifiable_text( $new_data ),
     478                        'Should have set the processing instruction data.'
     479                );
     480
     481                $this->assertSame(
     482                        $new_data,
     483                        $processor->get_modifiable_text(),
     484                        'Should have read back the enqueued data before flushing the update.'
     485                );
     486
     487                $this->assertSame(
     488                        $expected,
     489                        $processor->get_updated_html(),
     490                        'Should have updated the document as expected.'
     491                );
     492
     493                $this->assertSame(
     494                        $new_data,
     495                        $processor->get_modifiable_text(),
     496                        'Should have read back the data after flushing the update.'
     497                );
     498
     499                $this->assertSame(
     500                        $target,
     501                        $processor->get_tag(),
     502                        'Should not have changed the processing instruction target.'
     503                );
     504
     505                $reparsed = new WP_HTML_Tag_Processor( $expected );
     506                $reparsed->next_token();
     507
     508                $this->assertSame(
     509                        '#processing-instruction',
     510                        $reparsed->get_token_name(),
     511                        'Should have found a processing instruction when re-parsing the updated document.'
     512                );
     513
     514                $this->assertSame(
     515                        $target,
     516                        $reparsed->get_tag(),
     517                        'Should have preserved the target when re-parsing the updated document.'
     518                );
     519
     520                $this->assertSame(
     521                        $new_data,
     522                        $reparsed->get_modifiable_text(),
     523                        'Should have found the set data when re-parsing the updated document.'
     524                );
     525        }
     526
     527        /**
     528         * Data provider.
     529         *
     530         * @return array<string, array{0: string, 1: string, 2: string}>
     531         */
     532        public static function data_processing_instruction_data_updates(): array {
     533                return array(
     534                        'Replace data'                     => array( '<?wp-bit before?>', 'after', '<?wp-bit after?>' ),
     535                        'Bare closer rewritten'            => array( '<?wp-bit before>', 'after', '<?wp-bit after?>' ),
     536                        'Separator inserted'               => array( '<?wp-bit?>', '{"just": "kidding"}', '<?wp-bit {"just": "kidding"}?>' ),
     537                        'Separator inserted (bare closer)' => array( '<?wp-bit>', 'data', '<?wp-bit data?>' ),
     538                        'Data abutting target'             => array( '<?wp-bit?data>', 'x', '<?wp-bit x?>' ),
     539                        'Data ending in ?'                 => array( '<?wp-bit d>', 'd?', '<?wp-bit d??>' ),
     540                        'Data ending in ? (?> closer)'     => array( '<?wp-bit x?>', 'd?', '<?wp-bit d??>' ),
     541                        'Data of only ?'                   => array( '<?wp-bit?>', '?', '<?wp-bit ??>' ),
     542                        'Emptied (?> closer)'              => array( '<?wp-bit data?>', '', '<?wp-bit ?>' ),
     543                        'Emptied (bare closer)'            => array( '<?wp-bit data>', '', '<?wp-bit ?>' ),
     544                        'Empty data set on empty data'     => array( '<?wp-bit?>', '', '<?wp-bit ?>' ),
     545                        'Whitespace run normalized'        => array( "<?wp-bit \t\n old?>", 'new', '<?wp-bit new?>' ),
     546                );
     547        }
     548
     549        /**
     550         * Ensures that repeated processing instruction data updates replace
     551         * each other instead of accumulating syntax adjustments.
     552         *
     553         * @ticket 61530
     554         */
     555        public function test_replaces_previous_processing_instruction_data_update(): void {
     556                $processor = new WP_HTML_Tag_Processor( '<?wp-bit?>' );
     557                $processor->next_token();
     558
     559                $this->assertTrue(
     560                        $processor->set_modifiable_text( 'first?' ),
     561                        'Should have set the initial processing instruction data.'
     562                );
     563
     564                $this->assertTrue(
     565                        $processor->set_modifiable_text( 'second' ),
     566                        'Should have replaced the pending processing instruction data.'
     567                );
     568
     569                $this->assertSame(
     570                        '<?wp-bit second?>',
     571                        $processor->get_updated_html(),
     572                        'Should have applied only the last update to the document.'
     573                );
     574        }
     575
     576        /**
    445577         * Ensures that updates with potentially-compromising values aren't accepted.
    446578         *
     
    453585         *
    454586         * @dataProvider data_unallowed_modifiable_text_updates
     587         *
     588         * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_modifiable_text
    455589         *
    456590         * @param string $html_with_nonempty_modifiable_text Will be used to find the test element.
     
    491625                        'Comment with -->'                        => array( '<!-- this is a comment -->', 'Comments end in -->' ),
    492626                        'Comment with --!>'                       => array( '<!-- this is a comment -->', 'Invalid but legitimate comments end in --!>' ),
     627                        'PI with >'                               => array( '<?wp-bit some data?>', 'Processing instructions end at the first >' ),
     628                        'PI with leading space'                   => array( '<?wp-bit some data?>', ' leading whitespace is skipped after the target' ),
     629                        'PI with leading tab'                     => array( '<?wp-bit some data?>', "\tleading whitespace is skipped after the target" ),
     630                        'PI with only whitespace'                 => array( '<?wp-bit some data?>', ' ' ),
    493631                        'Non-JS SCRIPT with <script>'             => array( '<script type="text/html">Replace me</script>', '<!-- Just a <script>' ),
    494632                        'Non-JS SCRIPT with </script>'            => array( '<script type="text/plain">Replace me</script>', 'Just a </script>' ),
     
    674812         * @ticket 64751
    675813         * @dataProvider data_set_modifiable_fails_non_atomic_tags
     814         *
     815         * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_modifiable_text
    676816         */
    677817        public function test_set_modifiable_fails_non_atomic_tags(
Note: See TracChangeset for help on using the changeset viewer.