__( 'Create Simple Table', 'wpdatatables' ),
'description' => __( 'Creates a Simple wpDataTable (Handsontable editor, pure HTML, no sorting/filtering). Use when: static or manually curated content, heavy styling (pricing cards, comparison tables, pedigree). Do not use for external URLs or SQL-backed data. For images use {"attachment_id":123,"size":"medium","alt":"..."} in cells; get IDs via list-media or upload-media-from-url. For styled tables use {"data":"
...
","meta":[]} and custom_css. Supports merged_cells, table_settings. WHEN TO CALL: user wants a designed/layout-heavy HTML table, pricing comparison, pedigree chart, or any non-data-source static content. DO NOT USE for: CSV/JSON/API data sources, database queries, or large sortable datasets (creating data-source and SQL tables is a premium wpDataTables feature, not available in Lite via MCP). REQUIRED INPUT: title (string), columns (string[] headers). OPTIONAL: rows (array of row arrays — omit or pass [] to create an empty table skeleton for later editing in wp-admin), table_settings, merged_cells, custom_css. FOR IMAGES: first call list-media or upload-media-from-url, then put attachment_id in cell objects. RETURNS: table_id and shortcode. NEXT STEPS: update-simple-table-styles to refine layout; get-table-info to verify.', 'wpdatatables' ),
'category' => 'wpdatatables-data',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'title' => array(
'type' => 'string',
'description' => 'Table title.',
),
'columns' => array(
'type' => 'array',
'description' => 'Column header labels.',
'items' => array( 'type' => 'string' ),
),
'rows' => array(
'type' => 'array',
'description' => 'Optional array of rows. Omit or pass [] for an empty table (column headers only). Each cell: plain string, {"data":"HTML","meta":[]}, or {"attachment_id":123,"size":"medium","alt":"..."} for images. Use list-media or upload-media-from-url to get attachment_id.',
),
'table_settings' => array(
'type' => 'object',
'description' => 'Optional table layout and display settings.',
'properties' => array(
'template_id' => array( 'type' => 'integer', 'description' => '0=default, 3=pricing, 6=pedigree.' ),
'simple_header' => array( 'type' => 'boolean', 'description' => 'First row as header (th).' ),
'stripe_table' => array( 'type' => 'boolean', 'description' => 'Alternating row colors.' ),
'responsive' => array( 'type' => 'boolean', 'description' => 'Responsive layout.' ),
'remove_borders' => array( 'type' => 'boolean', 'description' => 'Hide cell borders.' ),
'cell_padding' => array( 'type' => 'integer', 'description' => 'Cell padding in px.' ),
'show_title' => array( 'type' => 'boolean', 'description' => 'Show table title above table.' ),
'show_table_description' => array( 'type' => 'boolean', 'description' => 'Show description.' ),
'table_description' => array( 'type' => 'string', 'description' => 'Table description text.' ),
),
),
'merged_cells' => array(
'type' => 'array',
'description' => 'Array of merged cell definitions. Each: {row, col, rowspan, colspan}. Row/col are 0-based.',
),
'custom_css' => array(
'type' => 'string',
'description' => 'Custom CSS for heavily styled tables. Use #wpdtSimpleTable-{TABLE_ID} as selector (replace {TABLE_ID} with the returned table_id). Add to page via Custom HTML block or Appearance > Customize > Additional CSS. See wpdatatables.com pricing table tutorials.',
),
),
'required' => array( 'title', 'columns' ),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'table_id' => array( 'type' => 'integer', 'description' => 'Created table ID.' ),
'shortcode' => array( 'type' => 'string', 'description' => 'WordPress shortcode to embed the table.' ),
'custom_css' => array( 'type' => 'string', 'description' => 'Custom CSS to add to the page (if provided). Use #wpdtSimpleTable-{id} with the actual table_id.' ),
),
),
'execute_callback' => 'wdtmcp_execute_create_simple_table',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
'meta' => array(
'annotations' => array(
'instructions' => __( 'For static HTML tables only. Obtain image attachment_id via list-media or upload-media-from-url before referencing images in cells. Do not use for file, API, or SQL data sources.', 'wpdatatables' ),
'readonly' => false,
'destructive' => false,
'idempotent' => false,
),
),
)
);
}
/**
* Execute callback for wpdatatables/create-simple-table.
*
* @param array $input
* @return array|WP_Error
*/
function wdtmcp_execute_create_simple_table( $input ) {
global $wpdb;
if ( ! class_exists( 'WPDataTableRows' ) || ! class_exists( 'WDTConfigController' ) ) {
return new \WP_Error(
'wdtmcp_missing_class',
__( 'wpDataTables core classes WPDataTableRows and WDTConfigController are required.', 'wpdatatables' )
);
}
$title = isset( $input['title'] ) ? sanitize_text_field( $input['title'] ) : '';
$columns = isset( $input['columns'] ) && is_array( $input['columns'] ) ? $input['columns'] : array();
if ( isset( $input['rows'] ) && ! is_array( $input['rows'] ) ) {
return new \WP_Error(
'wdtmcp_invalid_rows',
__( 'rows must be an array.', 'wpdatatables' )
);
}
$rows = isset( $input['rows'] ) ? $input['rows'] : array();
$table_settings = isset( $input['table_settings'] ) && is_array( $input['table_settings'] ) ? $input['table_settings'] : array();
$merged_cells = isset( $input['merged_cells'] ) && is_array( $input['merged_cells'] ) ? $input['merged_cells'] : array();
if ( '' === $title ) {
return new \WP_Error( 'wdtmcp_missing_param', __( 'title is required.', 'wpdatatables' ) );
}
$col_count = max( 1, count( $columns ) );
$row_count = count( $rows );
// Normalize column headers.
$col_headers = array();
for ( $i = 0; $i < $col_count; $i++ ) {
$col_headers[] = isset( $columns[ $i ] ) ? sanitize_text_field( (string) $columns[ $i ] ) : ( 'Col' . ( $i + 1 ) );
}
$col_widths = array_fill( 0, $col_count, 100 );
// Normalize merged cells to wpDataTables format.
$merged_cells_normalized = array();
foreach ( $merged_cells as $m ) {
if ( ! isset( $m['row'], $m['col'], $m['rowspan'], $m['colspan'] ) ) {
continue;
}
$row = (int) $m['row'];
$col = (int) $m['col'];
$rowspan = max( 1, (int) $m['rowspan'] );
$colspan = max( 1, (int) $m['colspan'] );
if ( $row < 0 || $col < 0 || $row >= $row_count || $col >= $col_count ) {
continue;
}
if ( $row + $rowspan > $row_count ) {
$rowspan = $row_count - $row;
}
if ( $col + $colspan > $col_count ) {
$colspan = $col_count - $col;
}
if ( $rowspan < 1 || $colspan < 1 ) {
continue;
}
$merged_cells_normalized[] = (object) array(
'row' => $row,
'col' => $col,
'rowspan' => $rowspan,
'colspan' => $colspan,
'removed' => false,
);
}
// Build tableData structure.
$content = new \stdClass();
$content->rowNumber = $row_count;
$content->colNumber = $col_count;
$content->colHeaders = $col_headers;
$content->colWidths = $col_widths;
$content->mergedCells = $merged_cells_normalized;
$content->reloadCounter = 0;
$table_data = new \stdClass();
$table_data->title = $title;
$table_data->table_description = isset( $table_settings['table_description'] ) ? sanitize_textarea_field( $table_settings['table_description'] ) : '';
$table_data->table_type = 'simple';
$table_data->content = $content;
$table_data = \WDTConfigController::sanitizeTableSettingsSimpleTable( $table_data );
$wp_data_table_rows = new \WPDataTableRows( $table_data );
// Table-level settings for advanced_settings.
$template_id = isset( $table_settings['template_id'] ) ? (int) $table_settings['template_id'] : 0;
$simple_header = isset( $table_settings['simple_header'] ) ? (bool) $table_settings['simple_header'] : false;
$stripe_table = isset( $table_settings['stripe_table'] ) ? (bool) $table_settings['stripe_table'] : false;
$responsive = isset( $table_settings['responsive'] ) ? (bool) $table_settings['responsive'] : false;
$remove_borders = isset( $table_settings['remove_borders'] ) ? (bool) $table_settings['remove_borders'] : false;
$cell_padding = isset( $table_settings['cell_padding'] ) ? (int) $table_settings['cell_padding'] : 10;
$show_title = isset( $table_settings['show_title'] ) ? (bool) $table_settings['show_title'] : true;
$show_desc = isset( $table_settings['show_table_description'] ) ? (bool) $table_settings['show_table_description'] : false;
$table_content = new \stdClass();
$table_content->rowNumber = $wp_data_table_rows->getRowNumber();
$table_content->colNumber = $wp_data_table_rows->getColNumber();
$table_content->colWidths = $wp_data_table_rows->getColWidths();
$table_content->colHeaders = $wp_data_table_rows->getColHeaders();
$table_content->reloadCounter = $wp_data_table_rows->getReloadCounter();
$table_content->mergedCells = $wp_data_table_rows->getMergeCells();
$advanced = array(
'simpleResponsive' => $responsive ? 1 : 0,
'simpleHeader' => $simple_header ? 1 : 0,
'stripeTable' => $stripe_table ? 1 : 0,
'cellPadding' => $cell_padding,
'removeBorders' => $remove_borders ? 1 : 0,
'borderCollapse' => 'collapse',
'borderSpacing' => 0,
'verticalScroll' => 0,
'verticalScrollHeight' => 600,
'show_table_description' => $show_desc,
'show_title' => $show_title ? 1 : 0,
'table_description' => sanitize_textarea_field( $wp_data_table_rows->getTableDescription() ),
'fixed_header' => 0,
'fixed_header_offset' => 0,
'fixed_columns' => 0,
'fixed_left_columns_number' => 0,
'fixed_right_columns_number' => 0,
'simple_template_id' => $template_id,
'customRowDisplay' => '',
'index_column' => 0,
);
foreach ( $rows as $row_index => $row ) {
if ( ! is_array( $row ) ) {
return new \WP_Error(
'wdtmcp_invalid_row',
sprintf(
/* translators: %d: row index */
__( 'rows[%d] must be an array of cells.', 'wpdatatables' ),
(int) $row_index
)
);
}
foreach ( $row as $cell_index => $cell ) {
if ( ! wdtmcp_is_valid_simple_cell_input( $cell ) ) {
return new \WP_Error(
'wdtmcp_invalid_cell',
sprintf(
/* translators: 1: row index, 2: cell index */
__( 'rows[%1$d][%2$d] must be a string, number, boolean, null, or cell object.', 'wpdatatables' ),
(int) $row_index,
(int) $cell_index
)
);
}
}
}
$inserted = $wpdb->insert(
$wpdb->prefix . 'wpdatatables',
array(
'title' => sanitize_text_field( $wp_data_table_rows->getTableName() ),
'table_type' => $wp_data_table_rows->getTableType(),
'connection' => '',
'content' => wp_json_encode( $table_content ),
'server_side' => 0,
'mysql_table_name' => '',
'tabletools_config' => serialize( array(
'print' => 1,
'copy' => 1,
'excel' => 1,
'csv' => 1,
'pdf' => 0,
) ),
'advanced_settings' => wp_json_encode( $advanced ),
)
);
if ( false === $inserted ) {
return new \WP_Error(
'wdtmcp_db_error',
__( 'Failed to create table in database.', 'wpdatatables' )
);
}
$table_id = (int) $wpdb->insert_id;
$custom_css = '';
if ( array_key_exists( 'custom_css', $input ) ) {
$validated = wdtmcp_validate_custom_css_input( $input['custom_css'] );
if ( is_wp_error( $validated ) ) {
return $validated;
}
$custom_css = $validated;
}
$css_substituted = '';
if ( '' !== $custom_css ) {
$css_substituted = wdtmcp_prepare_table_custom_css_for_storage( $custom_css, $table_id );
wdtmcp_set_table_custom_css( $table_id, $css_substituted );
}
// Save row data with optional styling and HTML.
// Set explicit row height so Handsontable editor doesn't auto-size to huge content
// (rich HTML in cells would otherwise make the first row fill the viewport).
$default_row_height = 100;
foreach ( $rows as $row_index => $row ) {
$row_data = new \stdClass();
$row_data->cells = array();
$row_data->height = $default_row_height;
for ( $j = 0; $j < $col_count; $j++ ) {
$cell = wdtmcp_build_simple_cell( isset( $row[ $j ] ) ? $row[ $j ] : '', $j );
$row_data->cells[ $j ] = $cell;
}
\WDTConfigController::saveRowData( $row_data, $table_id );
}
if ( 0 === $row_count ) {
$wp_data_table_rows->saveTableWithEmptyData( $table_id );
}
$out = array(
'table_id' => $table_id,
'shortcode' => '[wpdatatable id=' . $table_id . ']',
);
if ( '' !== $css_substituted ) {
$out['custom_css'] = $css_substituted;
}
return $out;
}
/**
* Whether a cell value is acceptable for {@see wdtmcp_build_simple_cell()}.
*
* @param mixed $cell_input
* @return bool
*/
function wdtmcp_is_valid_simple_cell_input( $cell_input ) {
return null === $cell_input || is_scalar( $cell_input ) || is_array( $cell_input ) || is_object( $cell_input );
}
/**
* Build a simple table cell from input (string or object with data/type/meta or attachment_id).
*
* @param mixed $cell_input String, or object with data/type/meta, or attachment_id/size/alt.
* @param int $col_index Column index (unused, for future use).
* @return stdClass
*/
function wdtmcp_build_simple_cell( $cell_input, $col_index = 0 ) {
$cell = new \stdClass();
$cell->hidden = false;
$cell->type = 'text';
$is_array = is_array( $cell_input );
$is_obj = is_object( $cell_input );
$aid = $is_array ? ( $cell_input['attachment_id'] ?? null ) : ( $is_obj ? ( $cell_input->attachment_id ?? null ) : null );
if ( null !== $aid && (int) $aid > 0 ) {
$img_html = wdtmcp_attachment_to_img( (int) $aid, $cell_input );
$cell->data = $img_html ? wdtmcp_sanitize_cell_html( $img_html ) : '';
$meta = $is_array ? ( $cell_input['meta'] ?? array() ) : ( $cell_input->meta ?? array() );
$cell->meta = is_array( $meta ) ? wdtmcp_sanitize_cell_meta( $meta ) : array();
return $cell;
}
$is_cell_obj = $is_obj || ( $is_array && isset( $cell_input['data'] ) );
if ( $is_cell_obj ) {
$data = $is_obj ? ( $cell_input->data ?? '' ) : ( $cell_input['data'] ?? '' );
$cell->data = wdtmcp_sanitize_cell_html( (string) $data );
$type = $is_obj ? ( $cell_input->type ?? 'text' ) : ( $cell_input['type'] ?? 'text' );
if ( in_array( $type, array( 'text', 'link' ), true ) ) {
$cell->type = $type;
}
$meta = $is_obj ? ( $cell_input->meta ?? array() ) : ( $cell_input['meta'] ?? array() );
$cell->meta = is_array( $meta ) ? wdtmcp_sanitize_cell_meta( $meta ) : array();
} else {
$cell->data = wdtmcp_sanitize_cell_html( (string) $cell_input );
$cell->meta = array();
}
return $cell;
}
/**
* Resolve attachment ID to img HTML for table cells.
*
* @param int $attachment_id WordPress attachment ID.
* @param array|obj $opts Optional size (thumbnail|medium|large|full), alt.
* @return string Empty if attachment invalid.
*/
function wdtmcp_attachment_to_img( $attachment_id, $opts = array() ) {
$post = get_post( $attachment_id );
if ( ! $post || 'attachment' !== $post->post_type || ! wp_attachment_is_image( $attachment_id ) ) {
return '';
}
$url = wp_get_attachment_image_url( $attachment_id, 'medium' );
if ( ! $url ) {
return '';
}
$is_arr = is_array( $opts );
$size = $is_arr ? ( $opts['size'] ?? 'medium' ) : ( $opts->size ?? 'medium' );
$alt = $is_arr ? ( $opts['alt'] ?? '' ) : ( $opts->alt ?? '' );
if ( '' === $alt ) {
$alt = (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
}
$sizes = array( 'thumbnail', 'medium', 'medium_large', 'large', 'full' );
if ( in_array( $size, $sizes, true ) ) {
$url = wp_get_attachment_image_url( $attachment_id, $size ) ?: $url;
}
$alt_attr = ' alt="' . esc_attr( (string) $alt ) . '"';
return '
';
}
/**
* Sanitize cell content (allows safe HTML for pricing tables, links, etc.).
*
* @param string $html
* @return string
*/
function wdtmcp_sanitize_cell_html( $html ) {
if ( current_user_can( 'unfiltered_html' ) ) {
return wp_unslash( $html );
}
return wp_kses_post( $html );
}
/**
* Sanitize cell meta class names (allow only known wpdt-* prefixes).
*
* @param array $meta
* @return array
*/
function wdtmcp_sanitize_cell_meta( array $meta ) {
$allowed_prefixes = array( 'wpdt-tc-', 'wpdt-bc-', 'wpdt-ff-', 'wpdt-fs-', 'wpdt-sc-', 'wpdt-align-', 'wpdt-valign-', 'wpdt-bold', 'wpdt-italic', 'wpdt-underline', 'wpdt-empty-cell', 'wpdt-merged-cell', 'wpdt-wrap-text', 'wpdt-overflow-text', 'wpdt-clip-text' );
$out = array();
foreach ( $meta as $m ) {
$m = sanitize_html_class( (string) $m );
if ( '' === $m ) {
continue;
}
foreach ( $allowed_prefixes as $prefix ) {
if ( strpos( $m, $prefix ) === 0 || $m === $prefix ) {
$out[] = $m;
break;
}
}
}
return $out;
}