: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @param array $matches Single Regex Match.
* @return string HTML A element with email address.
function _make_email_clickable_cb( $matches ) {
$email = $matches[2] . '@' . $matches[3];
return $matches[1] . "<a href=\"mailto:{$email}\">{$email}</a>";
* Helper function used to build the "rel" attribute for a URL when creating an anchor using make_clickable().
* @param string $url The URL.
* @return string The rel attribute for the anchor or an empty string if no rel attribute should be added.
function _make_clickable_rel_attr( $url ) {
$scheme = strtolower( wp_parse_url( $url, PHP_URL_SCHEME ) );
$nofollow_schemes = array_intersect( wp_allowed_protocols(), array( 'https', 'http' ) );
// Apply "nofollow" to external links with qualifying URL schemes (mailto:, tel:, etc... shouldn't be followed).
if ( ! wp_is_internal_link( $url ) && in_array( $scheme, $nofollow_schemes, true ) ) {
$rel_parts[] = 'nofollow';
// Apply "ugc" when in comment context.
if ( 'comment_text' === current_filter() ) {
$rel = implode( ' ', $rel_parts );
* Filters the rel value that is added to URL matches converted to links.
* @param string $rel The rel value.
* @param string $url The matched URL being converted to a link tag.
$rel = apply_filters( 'make_clickable_rel', $rel, $url );
$rel_attr = $rel ? ' rel="' . esc_attr( $rel ) . '"' : '';
* Converts plaintext URI to HTML links.
* Converts URI, www and ftp, and email addresses. Finishes by fixing links
* @param string $text Content to convert URIs.
* @return string Content with converted URIs.
function make_clickable( $text ) {
$textarr = preg_split( '/(<[^<>]+>)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); // Split out HTML tags.
$nested_code_pre = 0; // Keep track of how many levels link is nested inside <pre> or <code>.
foreach ( $textarr as $piece ) {
if ( preg_match( '|^<code[\s>]|i', $piece )
|| preg_match( '|^<pre[\s>]|i', $piece )
|| preg_match( '|^<script[\s>]|i', $piece )
|| preg_match( '|^<style[\s>]|i', $piece )
} elseif ( $nested_code_pre
&& ( '</code>' === strtolower( $piece )
|| '</pre>' === strtolower( $piece )
|| '</script>' === strtolower( $piece )
|| '</style>' === strtolower( $piece )
|| ( '<' === $piece[0] && ! preg_match( '|^<\s*[\w]{1,20}+://|', $piece ) )
// Long strings might contain expensive edge cases...
if ( 10000 < strlen( $piece ) ) {
foreach ( _split_str_by_whitespace( $piece, 2100 ) as $chunk ) { // 2100: Extra room for scheme and leading and trailing parentheses.
if ( 2101 < strlen( $chunk ) ) {
$r .= $chunk; // Too big, no whitespace: bail.
$r .= make_clickable( $chunk );
$ret = " $piece "; // Pad with whitespace to simplify the regexes.
([\\s(<.,;:!?]) # 1: Leading whitespace, or punctuation.
[\\w]{1,20}+:// # Scheme and hier-part prefix.
(?=\S{1,2000}\s) # Limit to URLs less than about 2000 characters long.
[\\w\\x80-\\xff#%\\~/@\\[\\]*(+=&$-]*+ # Non-punctuation URL character.
(?: # Unroll the Loop: Only allow punctuation URL character if followed by a non-punctuation URL character.
[\'.,;:!?)] # Punctuation URL character.
[\\w\\x80-\\xff#%\\~/@\\[\\]*(+=&$-]++ # Non-punctuation URL character.
(\)?) # 3: Trailing closing parenthesis (for parenthesis balancing post processing).
* The regex is a non-anchored pattern and does not have a single fixed starting character.
* Tell PCRE to spend more time optimizing since, when used on a page load, it will probably be used several times.
$ret = preg_replace_callback( $url_clickable, '_make_url_clickable_cb', $ret );
$ret = preg_replace_callback( '#([\s>])((www|ftp)\.[\w\\x80-\\xff\#$%&~/.\-;:=,?@\[\]+]+)#is', '_make_web_ftp_clickable_cb', $ret );
$ret = preg_replace_callback( '#([\s>])([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})#i', '_make_email_clickable_cb', $ret );
$ret = substr( $ret, 1, -1 ); // Remove our whitespace padding.
// Cleanup of accidental links within links.
return preg_replace( '#(<a([ \r\n\t]+[^>]+?>|>))<a [^>]+?>([^>]+?)</a></a>#i', '$1$3</a>', $r );
* Breaks a string into chunks by splitting at whitespace characters.
* The length of each returned chunk is as close to the specified length goal as possible,
* with the caveat that each chunk includes its trailing delimiter.
* Chunks longer than the goal are guaranteed to not have any inner whitespace.
* Joining the returned chunks with empty delimiters reconstructs the input string losslessly.
* Input string must have no null characters (or eventual transformations on output chunks must not care about null characters)
* _split_str_by_whitespace( "1234 67890 1234 67890a cd 1234 890 123456789 1234567890a 45678 1 3 5 7 90 ", 10 ) ==
* 0 => '1234 67890 ', // 11 characters: Perfect split.
* 1 => '1234 ', // 5 characters: '1234 67890a' was too long.
* 2 => '67890a cd ', // 10 characters: '67890a cd 1234' was too long.
* 3 => '1234 890 ', // 11 characters: Perfect split.
* 4 => '123456789 ', // 10 characters: '123456789 1234567890a' was too long.
* 5 => '1234567890a ', // 12 characters: Too long, but no inner whitespace on which to split.
* 6 => ' 45678 ', // 11 characters: Perfect split.
* 7 => '1 3 5 7 90 ', // 11 characters: End of $text.
* @param string $text The string to split.
* @param int $goal The desired chunk length.
* @return array Numeric array of chunks.
function _split_str_by_whitespace( $text, $goal ) {
$string_nullspace = strtr( $text, "\r\n\t\v\f ", "\000\000\000\000\000\000" );
while ( $goal < strlen( $string_nullspace ) ) {
$pos = strrpos( substr( $string_nullspace, 0, $goal + 1 ), "\000" );
$pos = strpos( $string_nullspace, "\000", $goal + 1 );
$chunks[] = substr( $text, 0, $pos + 1 );
$text = substr( $text, $pos + 1 );
$string_nullspace = substr( $string_nullspace, $pos + 1 );
* Callback to add a rel attribute to HTML A element.
* Will remove already existing string before adding to prevent invalidating (X)HTML.
* @param array $matches Single match.
* @param string $rel The rel attribute to add.
* @return string HTML A element with the added rel attribute.
function wp_rel_callback( $matches, $rel ) {
$atts = wp_kses_hair( $matches[1], wp_allowed_protocols() );
if ( ! empty( $atts['href'] ) && wp_is_internal_link( $atts['href']['value'] ) ) {
$rel = trim( str_replace( 'nofollow', '', $rel ) );
if ( ! empty( $atts['rel'] ) ) {
$parts = array_map( 'trim', explode( ' ', $atts['rel']['value'] ) );
$rel_array = array_map( 'trim', explode( ' ', $rel ) );
$parts = array_unique( array_merge( $parts, $rel_array ) );
$rel = implode( ' ', $parts );
foreach ( $atts as $name => $value ) {
if ( isset( $value['vless'] ) && 'y' === $value['vless'] ) {
$html .= "{$name}=\"" . esc_attr( $value['value'] ) . '" ';
$rel_attr = $rel ? ' rel="' . esc_attr( $rel ) . '"' : '';
return "<a {$text}{$rel_attr}>";
* Adds `rel="nofollow"` string to all HTML A elements in content.
* @param string $text Content that may contain HTML A elements.
* @return string Converted content.
function wp_rel_nofollow( $text ) {
// This is a pre-save filter, so text is already escaped.
$text = stripslashes( $text );
$text = preg_replace_callback(
static function ( $matches ) {
return wp_rel_callback( $matches, 'nofollow' );
return wp_slash( $text );
* Callback to add `rel="nofollow"` string to HTML A element.
* @deprecated 5.3.0 Use wp_rel_callback()
* @param array $matches Single match.
* @return string HTML A Element with `rel="nofollow"`.
function wp_rel_nofollow_callback( $matches ) {
return wp_rel_callback( $matches, 'nofollow' );
* Adds `rel="nofollow ugc"` string to all HTML A elements in content.
* @param string $text Content that may contain HTML A elements.
* @return string Converted content.
function wp_rel_ugc( $text ) {
// This is a pre-save filter, so text is already escaped.
$text = stripslashes( $text );
$text = preg_replace_callback(
static function ( $matches ) {
return wp_rel_callback( $matches, 'nofollow ugc' );
return wp_slash( $text );
* Adds `rel="noopener"` to all HTML A elements that have a target.
* @since 5.6.0 Removed 'noreferrer' relationship.
* @param string $text Content that may contain HTML A elements.
* @return string Converted content.
function wp_targeted_link_rel( $text ) {
// Don't run (more expensive) regex if no links with targets.
if ( stripos( $text, 'target' ) === false || stripos( $text, '<a ' ) === false || is_serialized( $text ) ) {
$script_and_style_regex = '/<(script|style).*?<\/\\1>/si';
preg_match_all( $script_and_style_regex, $text, $matches );
$extra_parts = $matches[0];
$html_parts = preg_split( $script_and_style_regex, $text );
foreach ( $html_parts as &$part ) {
$part = preg_replace_callback( '|<a\s([^>]*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $part );
for ( $i = 0; $i < count( $html_parts ); $i++ ) {
$text .= $html_parts[ $i ];
if ( isset( $extra_parts[ $i ] ) ) {
$text .= $extra_parts[ $i ];
* Callback to add `rel="noopener"` string to HTML A element.
* Will not duplicate an existing 'noopener' value to avoid invalidating the HTML.
* @since 5.6.0 Removed 'noreferrer' relationship.
* @param array $matches Single match.
* @return string HTML A Element with `rel="noopener"` in addition to any existing values.
function wp_targeted_link_rel_callback( $matches ) {
$link_html = $matches[1];
$original_link_html = $link_html;
// Consider the HTML escaped if there are no unescaped quotes.
$is_escaped = ! preg_match( '/(^|[^\\\\])[\'"]/', $link_html );
// Replace only the quotes so that they are parsable by wp_kses_hair(), leave the rest as is.
$link_html = preg_replace( '/\\\\([\'"])/', '$1', $link_html );
$atts = wp_kses_hair( $link_html, wp_allowed_protocols() );
* Filters the rel values that are added to links with `target` attribute.
* @param string $rel The rel values.
* @param string $link_html The matched content of the link tag including all HTML attributes.
$rel = apply_filters( 'wp_targeted_link_rel', 'noopener', $link_html );
// Return early if no rel values to be added or if no actual target attribute.
if ( ! $rel || ! isset( $atts['target'] ) ) {
return "<a $original_link_html>";
if ( isset( $atts['rel'] ) ) {
$all_parts = preg_split( '/\s/', "{$atts['rel']['value']} $rel", -1, PREG_SPLIT_NO_EMPTY );
$rel = implode( ' ', array_unique( $all_parts ) );
$atts['rel']['whole'] = 'rel="' . esc_attr( $rel ) . '"';
$link_html = implode( ' ', array_column( $atts, 'whole' ) );
$link_html = preg_replace( '/[\'"]/', '\\\\$0', $link_html );
* Adds all filters modifying the rel attribute of targeted links.
function wp_init_targeted_link_rel_filters() {
'content_filtered_save_pre',
foreach ( $filters as $filter ) {
add_filter( $filter, 'wp_targeted_link_rel' );
* Removes all filters modifying the rel attribute of targeted links.
function wp_remove_targeted_link_rel_filters() {
'content_filtered_save_pre',
foreach ( $filters as $filter ) {
remove_filter( $filter, 'wp_targeted_link_rel' );
* Converts one smiley code to the icon graphic file equivalent.
* Callback handler for convert_smilies().
* Looks up one smiley code in the $wpsmiliestrans global array and returns an
* `<img>` string for that smiley.
* @global array $wpsmiliestrans
* @param array $matches Single match. Smiley code to convert to image.
* @return string Image string for smiley.
function translate_smiley( $matches ) {
if ( count( $matches ) === 0 ) {
$smiley = trim( reset( $matches ) );
$img = $wpsmiliestrans[ $smiley ];
$ext = preg_match( '/\.([^.]+)$/', $img, $matches ) ? strtolower( $matches[1] ) : false;
$image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif' );
// Don't convert smilies that aren't images - they're probably emoji.
if ( ! in_array( $ext, $image_exts, true ) ) {
* Filters the Smiley image URL before it's used in the image element.
* @param string $smiley_url URL for the smiley image.
* @param string $img Filename for the smiley image.
* @param string $site_url Site URL, as returned by site_url().
$src_url = apply_filters( 'smilies_src', includes_url( "images/smilies/$img" ), $img, site_url() );
return sprintf( '<img src="%s" alt="%s" class="wp-smiley" style="height: 1em; max-height: 1em;" />', esc_url( $src_url ), esc_attr( $smiley ) );
* Converts text equivalent of smilies to images.
* Will only convert smilies if the option 'use_smilies' is true and the global
* used in the function isn't empty.
* @global string|array $wp_smiliessearch
* @param string $text Content to convert smilies from text.
* @return string Converted content with text smilies replaced with images.
function convert_smilies( $text ) {
global $wp_smiliessearch;
if ( get_option( 'use_smilies' ) && ! empty( $wp_smiliessearch ) ) {
// HTML loop taken from texturize function, could possible be consolidated.
$textarr = preg_split( '/(<.*>)/U', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); // Capture the tags as well as in between.
$stop = count( $textarr ); // Loop stuff.
// Ignore processing of specific tags.
$tags_to_ignore = 'code|pre|style|script|textarea';
$ignore_block_element = '';