: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* Contact Form 7's class used for formatting HTML fragments.
class WPCF7_HTMLFormatter {
* Tag name reserved for a custom HTML element used as a block placeholder.
const placeholder_block = 'placeholder:block';
* Tag name reserved for a custom HTML element used as an inline placeholder.
const placeholder_inline = 'placeholder:inline';
* The void elements in HTML.
* @link https://developer.mozilla.org/en-US/docs/Glossary/Void_element
const void_elements = array(
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr',
self::placeholder_block, self::placeholder_inline,
* HTML elements that can contain flow content.
const p_parent_elements = array(
'address', 'article', 'aside', 'blockquote', 'body', 'caption',
'dd', 'details', 'dialog', 'div', 'dt', 'fieldset', 'figcaption',
'figure', 'footer', 'form', 'header', 'li', 'main', 'nav',
* HTML elements that can be neither the parent nor a child of
const p_nonparent_elements = array(
'colgroup', 'dl', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head',
'hgroup', 'html', 'legend', 'menu', 'ol', 'pre', 'style', 'summary',
'table', 'tbody', 'template', 'tfoot', 'thead', 'title', 'tr', 'ul',
* HTML elements in the phrasing content category, plus non-phrasing
* content elements that can be grandchildren of a paragraph element.
const p_child_elements = array(
'a', 'abbr', 'area', 'audio', 'b', 'bdi', 'bdo', 'br', 'button',
'canvas', 'cite', 'code', 'data', 'datalist', 'del', 'dfn',
'em', 'embed', 'i', 'iframe', 'img', 'input', 'ins', 'kbd',
'keygen', 'label', 'link', 'map', 'mark', 'meta',
'meter', 'noscript', 'object', 'output', 'picture', 'progress',
'q', 'ruby', 's', 'samp', 'script', 'select', 'slot', 'small',
'span', 'strong', 'sub', 'sup', 'textarea',
'time', 'u', 'var', 'video', 'wbr',
'optgroup', 'option', 'rp', 'rt', // non-phrasing grandchildren
self::placeholder_inline,
* HTML elements that can contain phrasing content.
const br_parent_elements = array(
'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi',
'bdo', 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code',
'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div',
'dt', 'em', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'i', 'ins', 'kbd',
'label', 'legend', 'li', 'main', 'map', 'mark', 'meter', 'nav',
'noscript', 'object', 'output', 'p', 'progress', 'q', 'rt',
'ruby', 's', 'samp', 'section', 'slot', 'small', 'span', 'strong',
'sub', 'summary', 'sup', 'td', 'th', 'time', 'u', 'var',
private $options = array();
private $stacked_elements = array();
public function __construct( $options = '' ) {
$this->options = wp_parse_args( $options, array(
* Separates the given text into chunks of HTML. Each chunk must be an
* associative array that includes 'position', 'type', and 'content' keys.
* @param string $input Text to be separated into chunks.
* @return iterable Iterable of chunks.
public function separate_into_chunks( $input ) {
$input_bytelength = strlen( $input );
while ( $position < $input_bytelength ) {
'/(?:<!--.*?-->|<(?:\/?)[a-z].*?>)/is',
'content' => substr( $input, $position ),
$next_tag = $matches[0][0];
$next_tag_position = $matches[0][1];
if ( $position < $next_tag_position ) {
$next_tag_position - $position
if ( '<!' === substr( $next_tag, 0, 2 ) ) {
$next_tag_type = self::comment;
} elseif ( '</' === substr( $next_tag, 0, 2 ) ) {
$next_tag_type = self::end_tag;
$next_tag_type = self::start_tag;
'position' => $next_tag_position,
'type' => $next_tag_type,
$position = $next_tag_position + strlen( $next_tag );
* Normalizes content in each chunk. This may change the type and position
* @param iterable $chunks The original chunks.
* @return iterable Normalized chunks.
public function pre_format( $chunks ) {
foreach ( $chunks as $chunk ) {
$chunk['position'] = $position;
// Standardize newline characters to "\n".
$chunk['content'] = str_replace(
array( "\r\n", "\r" ), "\n", $chunk['content']
if ( $chunk['type'] === self::start_tag ) {
list( $chunk['content'] ) =
self::normalize_start_tag( $chunk['content'] );
// Replace <br /> by a line break.
$this->options['auto_br'] and
preg_match( '/^<br\s*\/?>$/i', $chunk['content'] )
$chunk['type'] = self::text;
$chunk['content'] = "\n";
$position = self::calc_next_position( $chunk );
* Concatenates neighboring text chunks to create a single chunk.
* @param iterable $chunks The original chunks.
* @return iterable Processed chunks.
public function concatenate_texts( $chunks ) {
foreach ( $chunks as $chunk ) {
$chunk['position'] = $position;
if ( $chunk['type'] === self::text ) {
if ( isset( $text_left ) ) {
$text_left['content'] .= $chunk['content'];
if ( isset( $text_left ) ) {
$chunk['position'] = self::calc_next_position( $text_left );
$position = self::calc_next_position( $chunk );
if ( isset( $text_left ) ) {
* Outputs formatted HTML based on the given chunks.
* @param iterable $chunks The original chunks.
* @return string Formatted HTML.
public function format( $chunks ) {
$chunks = $this->pre_format( $chunks );
$chunks = $this->concatenate_texts( $chunks );
$this->stacked_elements = array();
foreach ( $chunks as $chunk ) {
if ( $chunk['type'] === self::text ) {
$this->append_text( $chunk['content'] );
if ( $chunk['type'] === self::start_tag ) {
$this->start_tag( $chunk['content'] );
if ( $chunk['type'] === self::end_tag ) {
$this->end_tag( $chunk['content'] );
if ( $chunk['type'] === self::comment ) {
$this->append_comment( $chunk['content'] );
// Close all remaining tags.
* Appends a text node content to the output property.
* @param string $content Text node content.
public function append_text( $content ) {
if ( $this->is_inside( array( 'pre', 'template' ) ) ) {
$this->output .= $content;
empty( $this->stacked_elements ) or
$this->has_parent( 'p' ) or
$this->has_parent( self::p_parent_elements )
// Close <p> if the content starts with multiple line breaks.
if ( preg_match( '/^\s*\n\s*\n\s*/', $content ) ) {
// Split up the contents into paragraphs, separated by double line breaks.
$paragraphs = preg_split( '/\s*\n\s*\n\s*/', $content );
$paragraphs = array_filter( $paragraphs, static function ( $paragraph ) {
return '' !== trim( $paragraph );
$paragraphs = array_values( $paragraphs );
if ( $this->is_inside( 'p' ) ) {
$paragraph = array_shift( $paragraphs );
$paragraph = self::normalize_paragraph(
$this->options['auto_br']
$this->output .= $paragraph;
foreach ( $paragraphs as $paragraph ) {
$paragraph = ltrim( $paragraph );
$paragraph = self::normalize_paragraph(
$this->options['auto_br']
$this->output .= $paragraph;
// Close <p> if the content ends with multiple line breaks.
if ( preg_match( '/\s*\n\s*\n\s*$/', $content ) ) {
// Cases where the content is a single line break.
if ( preg_match( '/^\s*\n\s*$/', $content ) ) {
$auto_br = $this->options['auto_br'] && $this->is_inside( 'p' );
$content = self::normalize_paragraph( $content, $auto_br );
$this->output .= $content;
$auto_br = $this->options['auto_br'] &&
$this->has_parent( self::br_parent_elements );
$content = self::normalize_paragraph( $content, $auto_br );
$this->output .= $content;
* Appends a start tag to the output property.
* @param string $tag A start tag.
public function start_tag( $tag ) {
list( $tag, $tag_name ) = self::normalize_start_tag( $tag );
if ( in_array( $tag_name, self::p_child_elements ) ) {
! $this->is_inside( 'p' ) and
! $this->is_inside( self::p_child_elements ) and
! $this->has_parent( self::p_nonparent_elements )
// Open <p> if it does not exist.
in_array( $tag_name, self::p_parent_elements ) or
in_array( $tag_name, self::p_nonparent_elements )
// Close <p> if it exists.
if ( 'dd' === $tag_name or 'dt' === $tag_name ) {
// Close <dd> and <dt> if closing tag is omitted.
if ( 'li' === $tag_name ) {
// Close <li> if closing tag is omitted.
if ( 'optgroup' === $tag_name ) {
// Close <option> and <optgroup> if closing tag is omitted.
$this->end_tag( 'option' );
$this->end_tag( 'optgroup' );
if ( 'option' === $tag_name ) {
// Close <option> if closing tag is omitted.
$this->end_tag( 'option' );
if ( 'rp' === $tag_name or 'rt' === $tag_name ) {
// Close <rp> and <rt> if closing tag is omitted.
if ( 'td' === $tag_name or 'th' === $tag_name ) {
// Close <td> and <th> if closing tag is omitted.
if ( 'tr' === $tag_name ) {
// Close <tr> if closing tag is omitted.
if ( 'tbody' === $tag_name or 'tfoot' === $tag_name ) {
// Close <thead> if closing tag is omitted.
$this->end_tag( 'thead' );
if ( 'tfoot' === $tag_name ) {
// Close <tbody> if closing tag is omitted.
$this->end_tag( 'tbody' );
if ( ! in_array( $tag_name, self::void_elements ) ) {
array_unshift( $this->stacked_elements, $tag_name );
if ( ! in_array( $tag_name, self::p_child_elements ) ) {
if ( '' !== $this->output ) {
$this->output = rtrim( $this->output ) . "\n";
if ( $this->options['auto_indent'] ) {
$this->output .= self::indent( count( $this->stacked_elements ) - 1 );
* Closes an element and its open descendants at a time.
* @param string $tag An end tag.
public function end_tag( $tag ) {
if ( preg_match( '/<\/(.+?)(?:\s|>)/', $tag, $matches ) ) {
$tag_name = strtolower( $matches[1] );
$tag_name = strtolower( $tag );
$stacked_elements = array_values( $this->stacked_elements );
$tag_position = array_search( $tag_name, $stacked_elements );
if ( false === $tag_position ) {
// Element groups that make up an indirect nesting structure.
// Descendant can contain ancestors.
static $nesting_families = array(
'ancestors' => array( 'dl', ),
'descendants' => array( 'dd', 'dt', ),
'ancestors' => array( 'ol', 'ul', 'menu', ),
'descendants' => array( 'li', ),
'ancestors' => array( 'table', ),
'descendants' => array( 'td', 'th', 'tr', 'thead', 'tbody', 'tfoot', ),
foreach ( $nesting_families as $family ) {