$post->ID,
'post_type' => $post->post_type,
'posts_per_page' => 1,
) );
$wp_the_query = $wp_query;
// Fire the enqueue hooks so plugins register their scripts for this page.
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WordPress action
do_action( 'wp_enqueue_scripts' );
// Capture wp_head output (scripts, styles, inline JS).
ob_start();
wp_head();
$head = ob_get_clean();
// Capture wp_footer output (deferred scripts, localized data).
ob_start();
wp_footer();
$footer = ob_get_clean();
// Restore original state.
$wp_query = $original_query;
$wp_the_query = $original_the_query;
if ( $original_post ) {
$GLOBALS['post'] = $original_post;
setup_postdata( $original_post );
} else {
unset( $GLOBALS['post'] );
}
return array(
'head' => $head,
'footer' => $footer,
);
}
/**
* Converts absolute site URLs to root-relative paths so they resolve
* correctly against whatever base URL the WebView uses.
*
* For example, http://localhost:10004/wp-includes/js/jquery.js becomes
* /wp-includes/js/jquery.js. This is critical because the mobile WebView
* may use a different host (e.g. 10.0.2.2 for Android emulator).
*
* @param string $html HTML containing absolute URLs.
* @return string HTML with site URLs made relative.
*/
private function relativize_asset_urls( $html ) {
if ( empty( $html ) ) {
return $html;
}
$site_url = untrailingslashit( site_url() );
return str_replace( $site_url, '', $html );
}
/**
* Extracts only tags.
if ( preg_match_all( '/#si', '', (string) $inner_html );
$stripped = preg_replace( '##si', '', $stripped );
$stripped = trim( wp_strip_all_tags( $stripped ) );
return ( null === $block_name || '' === $block_name ) && '' === $stripped;
}
/**
* Maps one parsed block into one or more SDUI components.
*
* @param array $block Parsed block.
* @param WP_Post $post Source post/page.
* @return array
*/
private function map_block_progressive( $block, $post ) {
$block_name = isset( $block['blockName'] ) ? (string) $block['blockName'] : '';
$inner_html = isset( $block['innerHTML'] ) ? (string) $block['innerHTML'] : '';
$attrs = ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) ) ? $block['attrs'] : array();
$shortcode_mapped = $this->map_woocommerce_shortcode_block( $block_name, $inner_html, $attrs );
if ( ! empty( $shortcode_mapped ) ) {
return $shortcode_mapped;
}
$core_mapped = $this->map_core_block( $block_name, $inner_html, $attrs, $block );
if ( ! empty( $core_mapped ) ) {
return $core_mapped;
}
$fallback_html = '';
if ( ! empty( $block_name ) && function_exists( 'render_block' ) ) {
$fallback_html = (string) render_block( $block );
} else {
$fallback_html = $inner_html;
if ( ! empty( $fallback_html ) && false !== strpos( $fallback_html, '[' ) && function_exists( 'do_shortcode' ) ) {
$fallback_html = do_shortcode( $fallback_html );
}
}
$fallback_html = preg_replace( '##si', '', $fallback_html );
$fallback_html = preg_replace( '##si', '', $fallback_html );
if ( '' === trim( wp_strip_all_tags( $fallback_html ) ) ) {
return array();
}
return array(
array(
'type' => 'BlockHtml',
'html' => $fallback_html,
),
);
}
/**
* Core 8 Gutenberg block mapping.
*
* @param string $block_name Block name.
* @param string $inner_html Inner html.
* @param array $attrs Block attrs.
* @param array $block Full block.
* @return array
*/
private function map_core_block( $block_name, $inner_html, $attrs, $block ) {
switch ( $block_name ) {
case 'core/paragraph':
$plain_text = trim( wp_strip_all_tags( $inner_html ) );
if ( $this->contains_shortcode_like_text( $plain_text ) ) {
return array();
}
$allowed = strip_tags( $inner_html, '' );
$text = trim( wp_strip_all_tags( $allowed ) );
if ( '' === $text ) {
return array();
}
return array(
array(
'type' => 'BlockText',
'html_content' => trim( $allowed ),
'text' => $text,
'style' => 'paragraph',
),
);
case 'core/heading':
$level = isset( $attrs['level'] ) ? (int) $attrs['level'] : 2;
$level = max( 1, min( 6, $level ) );
$allowed = strip_tags( $inner_html, '' );
return array(
array(
'type' => 'BlockText',
'html_content' => trim( $allowed ),
'text' => trim( wp_strip_all_tags( $allowed ) ),
'style' => 'h' . $level,
),
);
case 'core/image':
$image = $this->parse_image_block( $block );
return empty( $image ) ? array() : array( $image );
case 'core/list':
$items = array();
if ( preg_match_all( '/]*>(.*?)<\/li>/si', $inner_html, $matches ) ) {
foreach ( $matches[1] as $item_html ) {
$item_text = trim( wp_strip_all_tags( $item_html ) );
if ( '' !== $item_text ) {
$items[] = $item_text;
}
}
}
if ( empty( $items ) ) {
return array();
}
return array(
array(
'type' => 'BlockList',
'items' => $items,
),
);
case 'core/quote':
$quote = $this->parse_quote_block( $block );
return empty( $quote ) ? array() : array( $quote );
case 'core/buttons':
$buttons = array();
if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as $inner ) {
$button = $this->parse_button_like_html( isset( $inner['innerHTML'] ) ? (string) $inner['innerHTML'] : '' );
if ( ! empty( $button ) ) {
$buttons[] = $button;
}
}
}
if ( empty( $buttons ) ) {
$button = $this->parse_button_like_html( $inner_html );
if ( ! empty( $button ) ) {
$buttons[] = $button;
}
}
return $buttons;
case 'core/gallery':
$images = array();
if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as $inner ) {
$img = $this->parse_image_block( $inner );
if ( ! empty( $img ) ) {
$images[] = $img;
}
}
}
if ( empty( $images ) && preg_match_all( '/
]+src=["\']([^"\']+)["\'][^>]*>/i', $inner_html, $matches ) ) {
foreach ( $matches[1] as $url ) {
$images[] = array(
'type' => 'BlockImage',
'url' => esc_url_raw( $url ),
'width' => 0,
'height' => 0,
'alt' => '',
);
}
}
if ( empty( $images ) ) {
return array();
}
return array(
array(
'type' => 'BlockGallery',
'images' => $images,
),
);
case 'core/embed':
$provider_url = '';
if ( isset( $attrs['url'] ) ) {
$provider_url = (string) $attrs['url'];
} elseif ( preg_match( '/https?:\/\/[^\s"\']+/i', $inner_html, $m ) ) {
$provider_url = $m[0];
}
if ( '' === $provider_url ) {
return array();
}
return array(
array(
'type' => 'BlockVideo',
'provider_url' => esc_url_raw( $provider_url ),
),
);
}
return array();
}
/**
* Detects and maps WooCommerce "Big 3" shortcodes.
*
* @param string $block_name Block name.
* @param string $inner_html Inner html.
* @param array $attrs Block attrs.
* @return array
*/
private function map_woocommerce_shortcode_block( $block_name, $inner_html, $attrs ) {
$shortcode_text = '';
if ( 'core/shortcode' === $block_name ) {
$shortcode_text = trim( wp_strip_all_tags( $inner_html ) );
} elseif ( 'core/paragraph' === $block_name ) {
$paragraph_text = trim( wp_strip_all_tags( $inner_html ) );
if ( $this->contains_shortcode_like_text( $paragraph_text ) ) {
$shortcode_text = $paragraph_text;
}
}
if ( '' === $shortcode_text ) {
return array();
}
if ( ! preg_match( '/^\[([a-zA-Z0-9_]+)([^\]]*)\]$/', $shortcode_text, $m ) ) {
return array();
}
$shortcode = strtolower( $m[1] );
$attr_str = isset( $m[2] ) ? trim( $m[2] ) : '';
$parsed = array();
if ( '' !== $attr_str ) {
$maybe = shortcode_parse_atts( $attr_str );
$parsed = is_array( $maybe ) ? $maybe : array();
}
if ( 'products' === $shortcode ) {
return array(
array(
'type' => 'ProductGrid',
'params' => array(
'ids' => isset( $parsed['ids'] ) ? (string) $parsed['ids'] : '',
'category' => isset( $parsed['category'] ) ? (string) $parsed['category'] : '',
'limit' => isset( $parsed['limit'] ) ? (string) $parsed['limit'] : '',
'columns' => isset( $parsed['columns'] ) ? (string) $parsed['columns'] : '',
),
'source' => 'shortcode',
),
);
}
if ( 'product_categories' === $shortcode ) {
return array(
array(
'type' => 'ProductCategoryList',
'params' => array(
'ids' => isset( $parsed['ids'] ) ? (string) $parsed['ids'] : '',
'number' => isset( $parsed['number'] ) ? (string) $parsed['number'] : '',
'parent' => isset( $parsed['parent'] ) ? (string) $parsed['parent'] : '',
'columns' => isset( $parsed['columns'] ) ? (string) $parsed['columns'] : '',
),
'source' => 'shortcode',
),
);
}
if ( 'add_to_cart' === $shortcode ) {
$product_id = isset( $parsed['id'] ) ? (string) $parsed['id'] : '';
if ( '' === $product_id ) {
return array();
}
return array(
array(
'type' => 'BlockButton',
'text' => isset( $parsed['text'] ) ? (string) $parsed['text'] : __( 'Add to cart', 'pressnative-apps' ),
'action' => array(
'type' => 'add_to_cart',
'payload' => array(
'product_id' => $product_id,
),
),
),
);
}
return array();
}
/**
* Parse a single anchor/button html snippet into BlockButton.
*
* @param string $html HTML snippet.
* @return array|null
*/
private function parse_button_like_html( $html ) {
if ( '' === trim( $html ) ) {
return null;
}
$text = trim( wp_strip_all_tags( $html ) );
$url = '';
if ( preg_match( '/]+href=["\']([^"\']+)["\']/i', $html, $m ) ) {
$url = esc_url_raw( $m[1] );
}
if ( '' === $text && '' === $url ) {
return null;
}
return array(
'type' => 'BlockButton',
'text' => $text,
'url' => $url,
);
}
/**
* Simple shortcode-like detector for paragraph/shortcode content.
*
* @param string $value Value to test.
* @return bool
*/
private function contains_shortcode_like_text( $value ) {
return (bool) preg_match( '/\[[a-zA-Z0-9_]+[^\]]*\]/', (string) $value );
}
/**
* Extracts image data from a core/image block.
*
* @param array $block Parsed image block.
* @return array|null BlockImage object or null.
*/
private function parse_image_block( $block ) {
$url = '';
$width = 0;
$height = 0;
$alt = '';
$html = $block['innerHTML'] ?? '';
if ( ! empty( $block['attrs']['id'] ) ) {
$attachment_id = (int) $block['attrs']['id'];
$image = wp_get_attachment_image_src( $attachment_id, 'large' );
if ( $image ) {
$url = $image[0];
$width = (int) $image[1];
$height = (int) $image[2];
}
$alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ?: '';
}
if ( empty( $url ) && preg_match( '/
]+src=["\']([^"\']+)["\']/', $html, $m ) ) {
$url = $m[1];
}
if ( empty( $width ) && preg_match( '/
]+width=["\']?(\d+)/', $html, $m ) ) {
$width = (int) $m[1];
}
if ( empty( $height ) && preg_match( '/
]+height=["\']?(\d+)/', $html, $m ) ) {
$height = (int) $m[1];
}
if ( empty( $alt ) && preg_match( '/
]+alt=["\']([^"\']*)["\']/', $html, $m ) ) {
$alt = $m[1];
}
if ( empty( $width ) && ! empty( $block['attrs']['width'] ) ) {
$width = (int) $block['attrs']['width'];
}
if ( empty( $height ) && ! empty( $block['attrs']['height'] ) ) {
$height = (int) $block['attrs']['height'];
}
if ( empty( $url ) ) {
return null;
}
return array(
'type' => 'BlockImage',
'url' => $url,
'width' => $width,
'height' => $height,
'alt' => $alt,
);
}
/**
* Extracts text and citation from a core/quote block.
*
* @param array $block Parsed quote block.
* @return array|null BlockQuote object or null.
*/
private function parse_quote_block( $block ) {
$html = $block['innerHTML'] ?? '';
$text = '';
$author = '';
if ( preg_match( '/]*>(.*?)<\/blockquote>/s', $html, $bq ) ) {
$inner = $bq[1];
if ( preg_match( '/(.*?)<\/cite>/s', $inner, $cite ) ) {
$author = trim( wp_strip_all_tags( $cite[1] ) );
$inner = str_replace( $cite[0], '', $inner );
}
$text = trim( wp_strip_all_tags( $inner ) );
}
if ( empty( $text ) ) {
return null;
}
return array(
'type' => 'BlockQuote',
'text' => $text,
'author' => $author,
);
}
/**
* Splits parsed Gutenberg blocks into segments at WooCommerce product
* shortcode boundaries. Used for inline-product mode.
*
* @param string $raw_content Raw post_content.
* @return array Segments — each is either ['type'=>'blocks','content_blocks'=>[...]]
* or ['type'=>'product','product_id'=>int].
*/
private function split_blocks_at_product_shortcodes( $raw_content ) {
if ( empty( $raw_content ) || ! function_exists( 'parse_blocks' ) ) {
return array();
}
$blocks = parse_blocks( $raw_content );
$product_pattern = '/\[(?:product_page|product|add_to_cart)\s+id=["\']?(\d+)["\']?[^\]]*\]/i';
$segments = array();
$current_blocks = array();
foreach ( $blocks as $block ) {
$block_name = $block['blockName'] ?? '';
$inner_html = $block['innerHTML'] ?? '';
if ( preg_match( $product_pattern, $inner_html, $m ) ) {
if ( ! empty( $current_blocks ) ) {
$segments[] = array( 'type' => 'blocks', 'content_blocks' => $current_blocks );
$current_blocks = array();
}
$segments[] = array( 'type' => 'product', 'product_id' => (int) $m[1] );
continue;
}
if ( in_array( $block_name, array( 'core/group', 'core/columns', 'core/column' ), true ) ) {
if ( ! empty( $block['innerBlocks'] ) ) {
$current_blocks = array_merge( $current_blocks, $this->map_gutenberg_blocks( $block['innerBlocks'] ) );
}
continue;
}
$mapped = $this->map_single_block( $block );
if ( null !== $mapped ) {
$current_blocks[] = $mapped;
}
}
if ( ! empty( $current_blocks ) ) {
$segments[] = array( 'type' => 'blocks', 'content_blocks' => $current_blocks );
}
return $segments;
}
/**
* Returns true if the content contains any WooCommerce product shortcodes.
*
* @param string $content Raw post content.
* @return bool
*/
private function has_product_shortcodes( $content ) {
if ( empty( $content ) ) {
return false;
}
return (bool) preg_match(
'/\[(?:product_page|product|add_to_cart)\s+id=["\']?\d+/i',
$content
);
}
/**
* Replaces WooCommerce product shortcodes with unique HTML-comment markers
* that survive the_content filters.
*
* e.g. [product_page id="76"] →
*
* @param string $content Raw post content (after native shortcodes are stripped).
* @return string Content with product shortcodes replaced by markers.
*/
private function replace_product_shortcodes_with_markers( $content ) {
return preg_replace_callback(
'/\[(?:product_page|product|add_to_cart)\s+id=["\']?(\d+)["\']?[^\]]*\]/i',
function( $m ) {
return '';
},
$content
);
}
/**
* Splits processed HTML at comment markers,
* returning an ordered array of segments.
*
* Each element is either:
* [ 'type' => 'html', 'content' => '...
' ]
* [ 'type' => 'product', 'product_id' => '76' ]
*
* @param string $html Fully processed HTML (after the_content + asset wrapping).
* @return array Ordered segments.
*/
private function split_html_at_product_markers( $html ) {
$segments = array();
$marker_pattern = '//';
$parts = preg_split( $marker_pattern, $html, -1, PREG_SPLIT_DELIM_CAPTURE );
// preg_split with PREG_SPLIT_DELIM_CAPTURE interleaves:
// [ text, product_id, text, product_id, text, ... ]
for ( $i = 0; $i < count( $parts ); $i++ ) {
if ( $i % 2 === 0 ) {
// HTML segment
$html_chunk = $parts[ $i ];
if ( '' !== trim( wp_strip_all_tags( $html_chunk ) ) ) {
$segments[] = array( 'type' => 'html', 'content' => $html_chunk );
} elseif ( '' !== trim( $html_chunk ) ) {
// Keep script/style tags even if no visible text (asset wrapping).
// Only add if it has