@@ -5020,6 +5020,158 @@ public function get_comment_type(): ?string {
50205020 return $ this ->is_virtual () ? null : parent ::get_comment_type ();
50215021 }
50225022
5023+ /**
5024+ * Normalize an HTML string by serializing it.
5025+ *
5026+ * This removes any partial syntax at the end of the string.
5027+ *
5028+ * @since 6.7.0
5029+ *
5030+ * @param string $html Input HTML to normalize.
5031+ *
5032+ * @return string|null Normalized output, or `null` if unable to normalize.
5033+ */
5034+ public static function normalize ( string $ html ): ?string {
5035+ return static ::create_fragment ( $ html )->serialize ();
5036+ }
5037+
5038+ /**
5039+ * Generate normalized markup for the HTML in the provided processor.
5040+ *
5041+ * This removes any partial syntax at the end of the string.
5042+ *
5043+ * @since 6.7.0
5044+ *
5045+ * @return string|null Normalized HTML markup represented by processor,
5046+ * or `null` if unable to generate serialization.
5047+ */
5048+ public function serialize (): ?string {
5049+ if ( WP_HTML_Tag_Processor::STATE_READY !== $ this ->parser_state ) {
5050+ return null ;
5051+ }
5052+
5053+ $ html = '' ;
5054+ while ( $ this ->next_token () ) {
5055+ $ token_type = $ this ->get_token_type ();
5056+
5057+ switch ( $ token_type ) {
5058+ case '#text ' :
5059+ $ html .= htmlspecialchars ( $ this ->get_modifiable_text (), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 , 'UTF-8 ' );
5060+ break ;
5061+
5062+ case '#funky-comment ' :
5063+ case '#comment ' :
5064+ $ html .= "<!-- {$ this ->get_modifiable_text ()}--> " ;
5065+ break ;
5066+
5067+ case '#cdata-section ' :
5068+ $ html .= "<![CDATA[ {$ this ->get_modifiable_text ()}]]> " ;
5069+ break ;
5070+
5071+ case 'html ' :
5072+ $ html .= '<!DOCTYPE html> ' ;
5073+ break ;
5074+ }
5075+
5076+ if ( '#tag ' !== $ token_type ) {
5077+ continue ;
5078+ }
5079+
5080+ if ( $ this ->is_tag_closer () ) {
5081+ $ html .= "</ {$ this ->get_qualified_tag_name ()}> " ;
5082+ continue ;
5083+ }
5084+
5085+ $ attribute_names = $ this ->get_attribute_names_with_prefix ( '' );
5086+ if ( ! isset ( $ attribute_names ) ) {
5087+ $ html .= "< {$ this ->get_qualified_tag_name ()}> " ;
5088+ continue ;
5089+ }
5090+
5091+ $ html .= "< {$ this ->get_qualified_tag_name ()}" ;
5092+ foreach ( $ attribute_names as $ attribute_name ) {
5093+ $ html .= " {$ this ->get_qualified_attribute_name ( $ attribute_name )}" ;
5094+ $ value = $ this ->get_attribute ( $ attribute_name );
5095+
5096+ if ( is_string ( $ value ) ) {
5097+ $ html .= '=" ' . htmlspecialchars ( $ value , ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 ) . '" ' ;
5098+ }
5099+ }
5100+
5101+ if ( 'html ' !== $ this ->get_namespace () && $ this ->has_self_closing_flag () ) {
5102+ $ html .= '/ ' ;
5103+ }
5104+
5105+ $ html .= '> ' ;
5106+ }
5107+
5108+ if ( null !== $ this ->get_last_error () ) {
5109+ return null ;
5110+ }
5111+
5112+ return $ html ;
5113+ }
5114+
5115+ /**
5116+ * Replaces the inner markup of the currently-matched tag with provided HTML.
5117+ *
5118+ * This function will normalize the given input and enforce the boundaries
5119+ * within the existing HTML where it's called.
5120+ *
5121+ * @since 6.8.0
5122+ *
5123+ * @param string $new_inner_html New HTML to inject as inner HTML for the currently-matched tag.
5124+ * @return bool Whether the inner markup was modified for the currently-matched tag, or `NULL`
5125+ * if called on a node which doesn't allow changing the inner HTML.
5126+ */
5127+ public function set_inner_html ( string $ new_inner_html ): ?bool {
5128+ $ tag_name = $ this ->get_tag ();
5129+
5130+ if (
5131+ WP_HTML_Tag_Processor::STATE_MATCHED_TAG !== $ this ->parser_state ||
5132+ $ this ->is_tag_closer () ||
5133+ ( 'html ' === $ this ->get_namespace () &&
5134+ (
5135+ self ::is_void ( $ tag_name ) ||
5136+ in_array ( $ tag_name , array ( 'IFRAME ' , 'NOEMBED ' , 'NOFRAMES ' , 'SCRIPT ' , 'STYLE ' , 'TEXTAREA ' , 'TITLE ' , 'XMP ' ), true )
5137+ )
5138+ )
5139+ ) {
5140+ // @todo Support setting inner HTML for SCRIPT, STYLE, TEXTAREA, and TITLE.
5141+ return null ;
5142+ }
5143+
5144+ $ fragment = $ this ->spawn_fragment_parser ( $ new_inner_html );
5145+ $ new_markup = $ fragment ->serialize ();
5146+
5147+ $ this ->set_bookmark ( 'start ' );
5148+ $ depth = $ this ->get_current_depth ();
5149+ while ( $ this ->get_current_depth () >= $ depth && $ this ->next_token () ) {
5150+ continue ;
5151+ }
5152+
5153+ if (
5154+ $ this ->paused_at_incomplete_token () ||
5155+ null !== $ this ->get_last_error ()
5156+ ) {
5157+ return false ;
5158+ }
5159+
5160+ $ this ->set_bookmark ( 'end ' );
5161+ $ start = $ this ->bookmarks ['_start ' ];
5162+ $ end = $ this ->bookmarks ['_end ' ];
5163+
5164+ $ this ->lexical_updates [] = new WP_HTML_Text_Replacement (
5165+ $ start ->start + $ start ->length ,
5166+ $ end ->start - ( $ start ->start + $ start ->length ),
5167+ $ new_markup
5168+ );
5169+
5170+ $ this ->get_updated_html ();
5171+ $ this ->seek ( 'start ' );
5172+ return true ;
5173+ }
5174+
50235175 /**
50245176 * Removes a bookmark that is no longer needed.
50255177 *
0 commit comments