Last active
February 26, 2026 17:47
-
-
Save mrfoxtalbot/ee2c8b35965c85ca122ee38e266699f1 to your computer and use it in GitHub Desktop.
A plugin that replaces WP Bakery's visual composer column shortcodes with native Gutenbeg blocks (use at you own discretion since inner content might not be parsed as expected)- You can user the Code Snippets plugin to run this code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /* | |
| Plugin Name: VC Columns to Native Blocks | |
| Description: Convierte [vc_row][vc_column]...[/vc_column][/vc_row] en bloques nativos de columnas, párrafos e imágenes. | |
| Author: Álvaro Gómez | |
| Version: 1.4.0 | |
| */ | |
| if ( ! defined( 'ABSPATH' ) ) { | |
| exit; | |
| } | |
| class AG_VC_Columns_To_Native { | |
| protected $last_report = null; | |
| public function __construct() { | |
| add_action( 'admin_menu', array( $this, 'register_tools_page' ) ); | |
| } | |
| public function register_tools_page() { | |
| add_management_page( | |
| 'Convert VC Columns', | |
| 'Convert VC Columns', | |
| 'manage_options', | |
| 'ag-vc-columns-to-native', | |
| array( $this, 'render_tools_page' ) | |
| ); | |
| } | |
| public function render_tools_page() { | |
| if ( ! current_user_can( 'manage_options' ) ) { | |
| return; | |
| } | |
| if ( isset( $_POST['ag_vc_convert_nonce'] ) && wp_verify_nonce( $_POST['ag_vc_convert_nonce'], 'ag_vc_convert' ) ) { | |
| $this->last_report = $this->run_conversion(); | |
| $this->print_report_notice(); | |
| } | |
| echo '<div class="wrap">'; | |
| echo '<h1>Convert VC Columns to Native Blocks</h1>'; | |
| echo '<p>Este script buscará patrones [vc_row][vc_column]...[/vc_column][/vc_row] en el contenido y los convertirá a bloques nativos de columnas, párrafos e imágenes.</p>'; | |
| echo '<form method="post">'; | |
| wp_nonce_field( 'ag_vc_convert', 'ag_vc_convert_nonce' ); | |
| submit_button( 'Run conversion' ); | |
| echo '</form>'; | |
| if ( $this->last_report ) { | |
| $this->print_detailed_report_table(); | |
| } | |
| echo '</div>'; | |
| } | |
| protected function print_report_notice() { | |
| if ( ! $this->last_report ) { | |
| return; | |
| } | |
| $r = $this->last_report; | |
| echo '<div class="notice notice-success is-dismissible">'; | |
| echo '<p><strong>Conversion finished.</strong></p>'; | |
| echo '<p>'; | |
| echo 'Posts scanned: ' . intval( $r['scanned'] ) . '<br>'; | |
| echo 'Posts updated: ' . intval( $r['updated'] ) . '<br>'; | |
| echo 'VC row groups converted: ' . intval( $r['groups_converted'] ) . '<br>'; | |
| echo 'VC rows skipped (unparsed): ' . intval( $r['rows_skipped'] ); | |
| echo '</p>'; | |
| if ( ! empty( $r['updated_ids'] ) ) { | |
| echo '<p>Updated post IDs: ' . esc_html( implode( ', ', $r['updated_ids'] ) ) . '</p>'; | |
| } | |
| echo '</div>'; | |
| } | |
| protected function print_detailed_report_table() { | |
| $r = $this->last_report; | |
| if ( empty( $r['details'] ) ) { | |
| return; | |
| } | |
| echo '<h2>Details</h2>'; | |
| echo '<table class="widefat striped">'; | |
| echo '<thead><tr>'; | |
| echo '<th>Post ID</th>'; | |
| echo '<th>Title</th>'; | |
| echo '<th>Rows found</th>'; | |
| echo '<th>Rows converted</th>'; | |
| echo '<th>Rows skipped</th>'; | |
| echo '</tr></thead>'; | |
| echo '<tbody>'; | |
| foreach ( $r['details'] as $post_id => $info ) { | |
| printf( | |
| '<tr><td>%d</td><td>%s</td><td>%d</td><td>%d</td><td>%d</td></tr>', | |
| $post_id, | |
| esc_html( get_the_title( $post_id ) ), | |
| intval( $info['rows_found'] ), | |
| intval( $info['rows_converted'] ), | |
| intval( $info['rows_skipped'] ) | |
| ); | |
| } | |
| echo '</tbody>'; | |
| echo '</table>'; | |
| } | |
| protected function run_conversion() { | |
| global $wpdb; | |
| $post_ids = $wpdb->get_col( | |
| "SELECT ID FROM {$wpdb->posts} | |
| WHERE post_type IN ('post','page') | |
| AND post_status IN ('publish','draft','pending','future','private') | |
| AND post_content LIKE '%[vc_row%'" | |
| ); | |
| $report = array( | |
| 'scanned' => 0, | |
| 'updated' => 0, | |
| 'groups_converted' => 0, | |
| 'rows_skipped' => 0, | |
| 'updated_ids' => array(), | |
| 'details' => array(), | |
| ); | |
| if ( empty( $post_ids ) ) { | |
| return $report; | |
| } | |
| foreach ( $post_ids as $post_id ) { | |
| $report['scanned']++; | |
| $content = get_post_field( 'post_content', $post_id ); | |
| $original = $content; | |
| $rows_found = 0; | |
| $rows_converted = 0; | |
| $rows_skipped = 0; | |
| // Match each complete vc_row block. | |
| $pattern = '/\[vc_row\b(.*?)\](.*?)\[\/vc_row\]/is'; | |
| if ( preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER ) ) { | |
| $rows_found = count( $matches ); | |
| foreach ( $matches as $match ) { | |
| $full_match = $match[0]; | |
| $row_inner = $match[2]; | |
| $converted = $this->convert_row_to_columns_block( $row_inner ); | |
| if ( $converted === null ) { | |
| $rows_skipped++; | |
| continue; | |
| } | |
| $rows_converted++; | |
| $content = str_replace( $full_match, $converted, $content ); | |
| } | |
| } | |
| if ( $content !== $original ) { | |
| $updated_post = array( | |
| 'ID' => $post_id, | |
| 'post_content' => $content, | |
| ); | |
| $result = wp_update_post( $updated_post, true ); | |
| if ( ! is_wp_error( $result ) ) { | |
| $report['updated']++; | |
| $report['groups_converted'] += $rows_converted; | |
| $report['rows_skipped'] += $rows_skipped; | |
| $report['updated_ids'][] = $post_id; | |
| $report['details'][ $post_id ] = array( | |
| 'rows_found' => $rows_found, | |
| 'rows_converted' => $rows_converted, | |
| 'rows_skipped' => $rows_skipped, | |
| ); | |
| } | |
| } | |
| } | |
| return $report; | |
| } | |
| /** | |
| * Convierte el contenido interno de un [vc_row] en un bloque <!-- wp:columns -->... | |
| * Devuelve null si no puede parsear columnas. | |
| */ | |
| protected function convert_row_to_columns_block( $row_inner ) { | |
| // Capturar todas las columnas de primer nivel dentro del row. | |
| $pattern = '/\[vc_column\b([^]]*)\](.*?)\[\/vc_column\]/is'; | |
| if ( ! preg_match_all( $pattern, $row_inner, $cols, PREG_SET_ORDER ) ) { | |
| return null; | |
| } | |
| $columns_html = array(); | |
| foreach ( $cols as $col_match ) { | |
| $col_attrs = $col_match[1]; | |
| $col_inner = $col_match[2]; | |
| // 1) Convertir [vc_column_text] a párrafos (dentro, <img> a bloques image). | |
| $col_inner = $this->convert_vc_column_text_to_paragraph( $col_inner ); | |
| // 2) Convertir <img> sueltos en la columna en bloques image (caso como el que comentas). | |
| $col_inner = $this->convert_imgs_to_image_blocks( $col_inner ); | |
| $columns_html[] = | |
| "<!-- wp:column -->\n" . | |
| '<div class="wp-block-column">' . $col_inner . "</div>\n" . | |
| "<!-- /wp:column -->"; | |
| } | |
| if ( empty( $columns_html ) ) { | |
| return null; | |
| } | |
| $columns_block = | |
| "<!-- wp:columns -->\n" . | |
| '<div class="wp-block-columns">' . "\n\n" . | |
| implode( "\n\n", $columns_html ) . "\n\n" . | |
| "</div>\n" . | |
| "<!-- /wp:columns -->"; | |
| return $columns_block; | |
| } | |
| /** | |
| * Reemplaza [vc_column_text]$content[/vc_column_text] por | |
| * <!-- wp:paragraph -->$content<!-- /wp:paragraph --> preservando $content, | |
| * y dentro de ese contenido convierte <img> en bloques de imagen. | |
| */ | |
| protected function convert_vc_column_text_to_paragraph( $content ) { | |
| $pattern = '/\[vc_column_text\b[^\]]*\](.*?)\[\/vc_column_text\]/is'; | |
| $callback = function ( $m ) { | |
| $inner = $m[1]; | |
| // Primero, convertir cualquier <img ...> dentro de $inner a bloques de imagen. | |
| $inner = $this->convert_imgs_to_image_blocks( $inner ); | |
| return "<!-- wp:paragraph -->" . $inner . "<!-- /wp:paragraph -->"; | |
| }; | |
| return preg_replace_callback( $pattern, $callback, $content ); | |
| } | |
| /** | |
| * Convierte <img> en bloques de imagen: | |
| * | |
| * <img src="..." class="... wp-image-8 ..." ...> | |
| * | |
| * pasa a: | |
| * | |
| * <!-- wp:image {"id":8,"sizeSlug":"large","linkDestination":"none"} --> | |
| * <figure class="wp-block-image size-large"><img src="..." alt="" class="wp-image-8"/></figure> | |
| * <!-- /wp:image --> | |
| * | |
| * El ID se toma del número en la clase wp-image-N. | |
| */ | |
| protected function convert_imgs_to_image_blocks( $html ) { | |
| $pattern = '/<img\b([^>]*)>/i'; | |
| $callback = function ( $m ) { | |
| $attrs_str = $m[1]; | |
| // Extraer src. | |
| $src = ''; | |
| if ( preg_match( '/\bsrc\s*=\s*"([^"]*)"/i', $attrs_str, $m_src ) ) { | |
| $src = $m_src[1]; | |
| } | |
| // Extraer clase completa (para conservar wp-image-N). | |
| $class_attr = ''; | |
| if ( preg_match( '/\bclass\s*=\s*"([^"]*)"/i', $attrs_str, $m_class ) ) { | |
| $class_attr = $m_class[1]; | |
| } | |
| // Extraer ID de wp-image-N. | |
| $id = 0; | |
| if ( preg_match( '/wp-image-([0-9]+)/', $class_attr, $m_id ) ) { | |
| $id = (int) $m_id[1]; | |
| } | |
| if ( empty( $src ) || $id <= 0 ) { | |
| // Si no tenemos src o id válido, no tocamos el <img>. | |
| return $m[0]; | |
| } | |
| // Asegurar que la clase wp-image-N está presente (y sólo una vez). | |
| $classes = preg_split( '/\s+/', trim( $class_attr ) ); | |
| if ( ! in_array( 'wp-image-' . $id, $classes, true ) ) { | |
| $classes[] = 'wp-image-' . $id; | |
| } | |
| $classes = array_filter( $classes ); | |
| $class_final = implode( ' ', $classes ); | |
| // Construir atributos del comentario del bloque. | |
| $comment_attrs = sprintf( | |
| '{"id":%d,"sizeSlug":"large","linkDestination":"none"}', | |
| $id | |
| ); | |
| // Figure estándar de bloque de imagen. | |
| $img_tag = sprintf( | |
| '<figure class="wp-block-image size-large"><img src="%1$s" alt="" class="%2$s"/></figure>', | |
| esc_url( $src ), | |
| esc_attr( $class_final ) | |
| ); | |
| $block = '<!-- wp:image ' . $comment_attrs . ' -->' . "\n"; | |
| $block .= $img_tag . "\n"; | |
| $block .= '<!-- /wp:image -->'; | |
| return $block; | |
| }; | |
| return preg_replace_callback( $pattern, $callback, $html ); | |
| } | |
| } | |
| new AG_VC_Columns_To_Native(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment