tags rendered by wrapper or sibling blocks. Nested * renders are rejected outright (see render() guard) so a single frame is * sufficient. * * @var array|null */ private static $current_render = null; /** * Render a form by post ID. * * @param int $form_id Form post ID. * @param string $context 'public' or 'editor'. * @param array $overrides Render-time overrides (e.g. host post ID for context). * @return string|WP_Error Rendered HTML on success, WP_Error on failure. */ public static function render( $form_id, $context = 'public', $overrides = [] ) { $form_id = absint( $form_id ); if ( ! $form_id ) { return new WP_Error( 'invalid_form_id', 'Invalid form ID.' ); } $form = get_post( $form_id ); if ( ! $form || GenerateBlocks_Pro_Form_Post_Type::POST_TYPE !== $form->post_type ) { return new WP_Error( 'form_not_found', 'Form not found.' ); } if ( null !== self::$current_render ) { return new WP_Error( 'form_render_recursion', 'Nested form renderer detected.' ); } // Public runtime: only render published forms. Editor preview allows // published forms for users who can use forms, but draft/private forms // still require edit access on the specific form post. if ( 'public' === $context ) { if ( 'publish' !== $form->post_status ) { return new WP_Error( 'form_not_published', 'Form not published.' ); } } else { if ( 'publish' !== $form->post_status && ! current_user_can( 'edit_post', $form_id ) ) { return new WP_Error( 'cannot_edit_form', 'Cannot preview this form.' ); } if ( 'publish' === $form->post_status && ! GenerateBlocks_Pro_Form_Post_Type::current_user_can( 'use' ) && ! current_user_can( 'edit_post', $form_id ) ) { return new WP_Error( 'cannot_edit_form', 'Cannot preview this form.' ); } } $meta = get_post_meta( $form_id, GenerateBlocks_Pro_Form_Post_Type::META_KEY, true ); $config = is_array( $meta ) && isset( $meta['config'] ) && is_array( $meta['config'] ) ? $meta['config'] : []; $host_post_id = isset( $overrides['host_post_id'] ) ? absint( $overrides['host_post_id'] ) : 0; // Bump the per-request instance counter and tell the form-field // renderer to suffix its IDs with it. Reset after `do_blocks()` so // nested rendering paths and tests aren't affected by leftover state. self::$instance_counter++; $instance = self::$instance_counter; self::$current_render = [ 'form_id' => $form_id, 'host_post_id' => $host_post_id, 'config' => $config, 'context' => $context, 'instance' => $instance, 'rendered_form_blocks' => 0, ]; if ( class_exists( 'GenerateBlocks_Block_Form_Field' ) ) { GenerateBlocks_Block_Form_Field::set_render_instance( $instance ); } try { $fields_html = self::render_fields( $form->post_content, $context ); $rendered_form_blocks = self::$current_render['rendered_form_blocks']; if ( 0 === $rendered_form_blocks ) { return new WP_Error( 'form_block_missing', 'Form block missing.' ); } if ( $rendered_form_blocks > 1 ) { return new WP_Error( 'form_multiple_blocks', 'Multiple Form blocks found.' ); } if ( '' === trim( $fields_html ) ) { return new WP_Error( 'form_empty', 'Form has no fields.' ); } return $fields_html; } finally { self::$current_render = null; if ( class_exists( 'GenerateBlocks_Block_Form_Field' ) ) { GenerateBlocks_Block_Form_Field::set_render_instance( 0 ); } } } /** * Render the inner field blocks. * * Uses do_blocks so the full render pipeline runs — dynamic tags, * conditional attributes, and generated classes stay consistent with how * inner form-field blocks render anywhere else. * * Editor preview runs inside a REST request — wp_head never fires, so * GenerateBlocks' default "queue CSS for wp_head" path drops the block * CSS on the floor. Force inline