or
tag - if so, return it unchanged.
// We need to check the raw post content before shortcodes are processed.
if ( $post instanceof WP_Post ) {
$raw_content = $post->post_content;
// Build a pattern to match the shortcode with any attributes.
$shortcode_name = RWPAI_SHORTCODE;
$shortcode_pattern = '/\[' . preg_quote( $shortcode_name, '/' ) . '(\s+[^\]]*)?\]/';
// Check if this shortcode pattern appears inside or tags.
// Match code/pre tags and their content.
if ( preg_match_all( '/<(code|pre)[^>]*>(.*?)<\/\1>/is', $raw_content, $code_matches ) ) {
foreach ( $code_matches[2] as $code_content ) {
// Check if our shortcode pattern appears in this code block.
if ( preg_match( $shortcode_pattern, $code_content ) ) {
// Shortcode is inside a code tag, return it as-is.
// Rebuild the shortcode text from attributes.
$attr_string = '';
if ( ! empty( $atts ) && is_array( $atts ) ) {
foreach ( $atts as $key => $value ) {
if ( null !== $value && '' !== $value ) {
$attr_string .= ' ' . $key . '="' . esc_attr( $value ) . '"';
}
}
}
return '[' . $shortcode_name . $attr_string . ']';
}
}
}
}
// Read more about shortcode attributes: https://wordpress.org/plugins/natural-text-to-speech.
$default_attribute = array(
'description' => null,
);
$atts = shortcode_atts( $default_attribute, $atts );
// Category exclusion logic.
$post_categories = array();
if ( $post instanceof WP_Post ) {
$post_categories = wp_get_post_terms(
$post->ID,
'category',
array( 'fields' => 'slugs' )
);
}
// Build HTML attributes.
$attributes = '';
foreach ( $default_attribute as $key => $default ) {
if ( ! empty( $atts[ $key ] ) ) {
$data_key = esc_attr( "data-$key" );
$data_val = esc_attr( $atts[ $key ] );
$attributes .= " {$data_key}=\"{$data_val}\"";
}
}
if ( $content ) {
$config = json_decode( rwpai_normalize_json_string_for_decode( $content ), true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return '' . esc_html__( 'Invalid JSON in shortcode', 'ai-agent' ) . '';
}
} else {
$config = rwpai_get_plugin_config();
}
if ( $config ) {
if ( isset( $config['exclude_from_post_categories'] ) ) {
$excluded = (array) $config['exclude_from_post_categories'];
// If any category matches, don't render the shortcode.
if ( array_intersect( $excluded, $post_categories ) ) {
return ''; // skip rendering.
}
}
}
if ( ! $content ) {
return sprintf(
'๐',
$attributes
);
}
$encoded = esc_attr( wp_json_encode( $config ) );
return sprintf(
'๐',
$attributes,
$encoded
);
}
add_shortcode( RWPAI_SHORTCODE, 'rwpai_plugin_shortcode' );
/**
* Normalize a "dirty" JSON string so it uses straight quotes and has no trailing commas.
*
* @param string $json Raw JSON input, possibly containing HTML entities or smart quotes.
* @return string Normalized JSON string safe for json_decode().
*/
function rwpai_normalize_json_string_for_decode( string $json ): string {
if ( '' === $json ) {
return $json;
}
$json = str_replace( array( '', '
', '
', '
' ), '', $json );
// 1) Remove UTF-8 BOM if present.
if ( "\xEF\xBB\xBF" === substr( $json, 0, 3 ) ) {
$json = substr( $json, 3 );
}
// 2) Decode HTML entities (e.g. “, “, etc.).
$json = html_entity_decode( $json, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
// 3) Replace common smart/directional quotes and apostrophes (UTF-8 byte sequences).
$search = array(
// double quotes variants.
"\xE2\x80\x9C", // U+201C โ .
"\xE2\x80\x9D", // U+201D โ .
"\xE2\x80\x9E", // U+201E โ .
"\xE2\x80\x9F", // U+201F โ .
// single quotes / apostrophes.
"\xE2\x80\x98", // U+2018 โ .
"\xE2\x80\x99", // U+2019 โ .
"\xE2\x80\xB9", // U+2039 โน .
"\xE2\x80\xBA", // U+203A โบ .
// left/right double angle quotes (often used in languages).
"\xC2\xAB", // U+00AB ยซ (in UTF-8) .
"\xC2\xBB", // U+00BB ยป .
);
$replace = array(
'"',
'"',
'"',
'"',
"'",
"'",
"'",
"'",
'"',
'"',
);
$json = str_replace( $search, $replace, $json );
// 5) Remove trailing commas before } or ] which break JSON.
$json = preg_replace( '/,\s*(?=[}\]])/u', '', $json );
// 6) Trim whitespace.
$json = trim( $json );
return $json;
}