$child ) { $name = strtolower( (string) $key ); if ( $attribute_context && ( preg_match( '/^on[a-z]+$/', $name ) || 'srcdoc' === $name ) ) { $errors[] = 'Executable HTML attributes are not permitted in layouts.'; } self::inspect_layout_value( $child, $errors, $url_context || self::is_layout_url_attribute( $name ) || (bool) preg_match( '/url$/', $name ), $attribute_context || in_array( $name, array( 'htmlattributes', 'customattributes' ), true ), $depth + 1 ); } } elseif ( is_string( $value ) ) { self::inspect_layout_html( $value, $errors ); if ( $url_context ) { self::inspect_layout_url( $value, $errors ); } } } /** Attribute names interpreted as URLs by native modules or HTML. */ private static function is_layout_url_attribute( $name ) { return in_array( strtolower( (string) $name ), array( 'href', 'xlink:href', 'src', 'url', 'action', 'formaction', 'poster', 'background' ), true ); } /** Inspect actual tags, leaving escaped examples and ordinary copy intact. */ private static function inspect_layout_html( $html, array &$errors, $embedded_depth = 0 ) { if ( preg_match( '/<\?(?:php\b|=)/i', $html ) ) { $errors[] = 'Executable PHP is not permitted in layouts.'; } if ( false === strpos( $html, '<' ) ) { return; } $processor = new WP_HTML_Tag_Processor( $html ); while ( $processor->next_tag() ) { if ( in_array( $processor->get_tag(), array( 'SCRIPT', 'OBJECT', 'EMBED', 'IFRAME', 'FORM', 'BASE', 'META', 'LINK' ), true ) ) { $errors[] = 'Executable or document-changing HTML elements are not permitted in layouts.'; } foreach ( $processor->get_attribute_names_with_prefix( '' ) ?? array() as $name ) { $name = strtolower( $name ); if ( preg_match( '/^on[a-z]+$/', $name ) || 'srcdoc' === $name ) { $errors[] = 'Executable HTML attributes are not permitted in layouts.'; } $value = $processor->get_attribute( $name ); if ( is_string( $value ) && self::is_layout_url_attribute( $name ) ) { self::inspect_layout_url( $value, $errors, $embedded_depth ); } if ( 'style' === $name && is_string( $value ) && preg_match( '/expression\s*\(/i', $value ) ) { $errors[] = 'Executable CSS expressions are not permitted in layouts.'; } } if ( in_array( $processor->get_tag(), array( 'ANIMATE', 'SET' ), true ) && self::is_layout_url_attribute( $processor->get_attribute( 'attributeName' ) ) ) { foreach ( array( 'from', 'to', 'values' ) as $attribute ) { foreach ( explode( ';', (string) $processor->get_attribute( $attribute ) ) as $url ) { self::inspect_layout_url( $url, $errors, $embedded_depth ); } } } } } /** Reject active URL schemes while allowing inert raster/SVG image data. */ private static function inspect_layout_url( $url, array &$errors, $embedded_depth = 0 ) { $url = html_entity_decode( (string) $url, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $normalized = preg_replace( '/[\x00-\x20\x7f]+/', '', $url ); if ( preg_match( '/^(?:javascript|vbscript):/i', $normalized ) ) { $errors[] = 'Executable URL schemes are not permitted in layouts.'; } if ( 0 !== stripos( $normalized, 'data:' ) || preg_match( '#^data:image/(?:png|gif|jpe?g|webp|avif|bmp|x-icon);base64,#i', $normalized ) ) { return; } if ( 4 > $embedded_depth && preg_match( '#^data:image/svg\+xml(?:;charset=[a-z0-9-]+)?(;base64)?,(.*)$#is', trim( $url ), $parts ) ) { $svg = ! empty( $parts[1] ) ? base64_decode( $parts[2], true ) : rawurldecode( $parts[2] ); if ( is_string( $svg ) && preg_match( '/ */ public static function ai_layout_reference() { $heading_attrs = self::module_attrs( array(), 'Reference heading' ); $heading_attrs['builderVersion'] = '5.11.0'; $heading_attrs['title']['innerContent']['desktop']['value'] = 'Editable native heading'; $image_attrs = self::module_attrs( array(), 'Reference image' ); $image_attrs['builderVersion'] = '5.11.0'; $image_attrs['image']['innerContent']['desktop']['value']['src'] = '{{VERIFIED_IMAGE_URL_FROM_USER_OR_SAVED_PAGE}}'; $blurb_attrs = self::module_attrs( array(), 'Reference feature blurb' ); $blurb_attrs['builderVersion'] = '5.11.0'; $blurb_attrs['imageIcon']['innerContent']['desktop']['value']['src'] = '{{VERIFIED_IMAGE_URL_FROM_USER_OR_SAVED_PAGE}}'; $blurb_attrs['title']['innerContent']['desktop']['value']['text'] = 'Editable feature title'; $blurb_attrs['content']['innerContent']['desktop']['value'] = '

Editable feature description.

'; $divider_attrs = self::module_attrs( array(), 'Reference divider' ); $divider_attrs['builderVersion'] = '5.11.0'; $reference_copy = self::leaf_block( 'divi/heading', $heading_attrs ); $reference_copy .= "\n" . self::text( '

Editable native paragraph copy.

', array(), array(), 'Reference paragraph' ); $reference_copy .= "\n" . self::button( 'Editable button', '#', array(), array(), 'Reference button' ); $reference_visual = self::leaf_block( 'divi/image', $image_attrs ); $reference_visual .= "\n" . self::leaf_block( 'divi/divider', $divider_attrs ); $reference_visual .= "\n" . self::leaf_block( 'divi/blurb', $blurb_attrs ); $document = self::placeholder( self::section( self::row( self::column( $reference_copy, array(), '14_24' ) . self::column( $reference_visual, array(), '10_24' ), array(), 'equal-columns_2' ), array(), array(), 'Reference responsive split section' ) ); return array( 'reference_version' => 'divi-powers-native-ai/1.1', 'trusted_source' => 'Packaged Divi Powers exports and native-layout evidence', 'blank_page_supported'=> true, 'preferred_builder_version' => '5.11.0', 'hierarchy' => array( 'divi/placeholder', 'divi/section', 'divi/row', 'divi/column', 'native leaf module' ), 'starter_document' => $document, 'confirmed_leaf_modules' => array( 'divi/heading' => 'title.innerContent.desktop.value', 'divi/text' => 'content.innerContent.desktop.value', 'divi/button' => 'button.innerContent.desktop.value.text', 'divi/image' => 'image.innerContent.desktop.value.src', 'divi/blurb' => array( 'imageIcon.innerContent.desktop.value.src', 'title.innerContent.desktop.value.text', 'content.innerContent.desktop.value', ), 'divi/divider' => 'No required content attributes', ), 'verified_design_leaf_assignments' => array( 'background' => array( array( 'p' => 'module.decoration.background..value.color', 'v' => '#F5F3FF' ), array( 'p' => 'module.decoration.background..value.enableColor', 'v' => 'on' ), ), 'spacing' => array( array( 'p' => 'module.decoration.spacing..value.padding.top', 'v' => '72px' ), array( 'p' => 'module.decoration.spacing..value.padding.right', 'v' => '6%' ), array( 'p' => 'module.decoration.spacing..value.padding.bottom', 'v' => '72px' ), array( 'p' => 'module.decoration.spacing..value.padding.left', 'v' => '6%' ), array( 'p' => 'module.decoration.spacing..value.padding.syncVertical', 'v' => 'on' ), array( 'p' => 'module.decoration.spacing..value.padding.syncHorizontal', 'v' => 'on' ), ), 'sizing' => array( array( 'p' => 'module.decoration.sizing..value.width', 'v' => '90%' ), array( 'p' => 'module.decoration.sizing..value.maxWidth', 'v' => '1120px' ), ), 'border' => array( array( 'p' => 'module.decoration.border..value.radius.sync', 'v' => 'on' ), array( 'p' => 'module.decoration.border..value.radius.topLeft', 'v' => '16px' ), array( 'p' => 'module.decoration.border..value.radius.topRight', 'v' => '16px' ), array( 'p' => 'module.decoration.border..value.radius.bottomRight', 'v' => '16px' ), array( 'p' => 'module.decoration.border..value.radius.bottomLeft', 'v' => '16px' ), array( 'p' => 'module.decoration.border..value.styles.all.width', 'v' => '1px' ), array( 'p' => 'module.decoration.border..value.styles.all.color', 'v' => '#DDD6FE' ), ), 'animation' => array( array( 'p' => 'module.decoration.animation..value.style', 'v' => 'fade' ), array( 'p' => 'module.decoration.animation..value.duration', 'v' => '700ms' ), array( 'p' => 'module.decoration.animation..value.delay', 'v' => '0ms' ), array( 'p' => 'module.decoration.animation..value.startingOpacity', 'v' => '0%' ), array( 'p' => 'module.decoration.animation..value.speedCurve', 'v' => 'ease-out' ), array( 'p' => 'module.decoration.animation..value.repeat', 'v' => 'once' ), ), 'text_heading_font' => array( array( 'p' => 'content.decoration.headingFont.h1.font..value.color', 'v' => '#24193D' ), array( 'p' => 'content.decoration.headingFont.h1.font..value.weight', 'v' => '700' ), array( 'p' => 'content.decoration.headingFont.h1.font..value.size', 'v' => '56px' ), array( 'p' => 'content.decoration.headingFont.h1.font..value.lineHeight', 'v' => '1.1em' ), ), 'text_body_font' => array( array( 'p' => 'content.decoration.bodyFont.body.font..value.color', 'v' => '#5F5870' ), array( 'p' => 'content.decoration.bodyFont.body.font..value.size', 'v' => '18px' ), array( 'p' => 'content.decoration.bodyFont.body.font..value.lineHeight', 'v' => '1.7em' ), ), 'button' => array( array( 'p' => 'button.decoration.font.font..value.color', 'v' => '#FFFFFF' ), array( 'p' => 'button.decoration.background..value.color', 'v' => '#6C3EE8' ), array( 'p' => 'button.decoration.background..value.enableColor', 'v' => 'on' ), array( 'p' => 'button.decoration.border..value.radius.sync', 'v' => 'on' ), array( 'p' => 'button.decoration.border..value.radius.topLeft', 'v' => '8px' ), array( 'p' => 'button.decoration.border..value.radius.topRight', 'v' => '8px' ), array( 'p' => 'button.decoration.border..value.radius.bottomRight', 'v' => '8px' ), array( 'p' => 'button.decoration.border..value.radius.bottomLeft', 'v' => '8px' ), ), ), 'block_specific_design_recipes' => array( 'section_surface' => array( 'applies_to' => 'divi/section', 'assignments' => array( array( 'p' => 'module.decoration.background..value.color', 'v' => '#FBF8F1' ), array( 'p' => 'module.decoration.background..value.enableColor', 'v' => 'on' ), array( 'p' => 'module.decoration.spacing..value.padding.top', 'v' => '88px' ), array( 'p' => 'module.decoration.spacing..value.padding.bottom', 'v' => '88px' ), ), ), 'responsive_split_row' => array( 'applies_to' => 'divi/row', 'assignments' => array( array( 'p' => 'module.advanced.flexColumnStructure.desktop.value', 'v' => 'equal-columns_2' ), array( 'p' => 'module.decoration.layout.desktop.value.display', 'v' => 'flex' ), array( 'p' => 'module.decoration.layout.desktop.value.flexWrap', 'v' => 'nowrap' ), array( 'p' => 'module.decoration.layout.tablet.value.flexDirection', 'v' => 'column' ), array( 'p' => 'module.decoration.layout.phone.value.flexDirection', 'v' => 'column' ), array( 'p' => 'module.decoration.sizing.desktop.value.width', 'v' => '90%' ), array( 'p' => 'module.decoration.sizing.desktop.value.maxWidth', 'v' => '1180px' ), ), ), 'ratio_column' => array( 'applies_to' => 'divi/column', 'assignments' => array( array( 'p' => 'module.decoration.sizing.desktop.value.flexType', 'v' => '11_24' ), array( 'p' => 'module.decoration.sizing.tablet.value.flexType', 'v' => '24_24' ), array( 'p' => 'module.decoration.sizing.phone.value.flexType', 'v' => '24_24' ), ), ), 'group_card_or_control_cluster' => array( 'applies_to' => 'divi/group', 'assignments' => array( array( 'p' => 'module.decoration.layout.desktop.value.display', 'v' => 'flex' ), array( 'p' => 'module.decoration.layout.desktop.value.flexWrap', 'v' => 'wrap' ), array( 'p' => 'module.decoration.background.desktop.value.color', 'v' => '#FFFFFF' ), array( 'p' => 'module.decoration.background.desktop.value.enableColor', 'v' => 'on' ), array( 'p' => 'module.decoration.border.desktop.value.radius.sync', 'v' => 'on' ), array( 'p' => 'module.decoration.border.desktop.value.radius.topLeft', 'v' => '18px' ), array( 'p' => 'module.decoration.border.desktop.value.radius.topRight', 'v' => '18px' ), array( 'p' => 'module.decoration.border.desktop.value.radius.bottomRight', 'v' => '18px' ), array( 'p' => 'module.decoration.border.desktop.value.radius.bottomLeft', 'v' => '18px' ), ), ), 'bounded_image' => array( 'applies_to' => 'divi/image', 'assignments' => array( array( 'p' => 'module.decoration.sizing.desktop.value.width', 'v' => '100%' ), array( 'p' => 'module.decoration.sizing.desktop.value.maxWidth', 'v' => '620px' ), array( 'p' => 'module.decoration.sizing.phone.value.width', 'v' => '100%' ), array( 'p' => 'module.decoration.sizing.phone.value.maxWidth', 'v' => '285px' ), ), ), ), 'content_tokens' => array( '{{VERIFIED_IMAGE_URL_FROM_USER_OR_SAVED_PAGE}} is schema evidence only and must never be emitted literally.', 'Attached mockups are visual references and are not reusable WordPress Media Library URLs.', ), ); } /** Return the packaged native-layout methodology used by Builder AI. */ public static function ai_methodology() { return array( 'version' => self::METHODOLOGY_VERSION, 'workflow' => array( 'inventory every visible page-body section, layer, overlap, asset, line break, width ratio, motif, and responsive change before generating markup', 'classify each visible element as native editable content, raster asset, or mixed composition', 'complete a section plan, asset manifest, percentage-based geometry plan, and responsive plan', 'plan sections, rows, columns, Groups, semantic modules, and breakpoint behavior from verified module evidence', 'preserve indispensable photographic, product, device, interface, texture, and illustration assets through approved native Image or background settings', 'generate balanced native Divi block markup from verified evidence', 'validate the complete native hierarchy without writing to WordPress', 'render at a comparable desktop width, compare the five largest visible deviations, correct them, render again, and verify a phone breakpoint before staging', ), 'evidence_priority' => array( 'current saved destination layout shapes and reusable site assets', 'packaged verified native module exports', 'packaged golden rendered layout reference', 'confirmed attribute paths and value shapes', 'simplify an unsupported detail instead of inventing a schema', ), 'hierarchy' => array( 'placeholder', 'section', 'row', 'column', 'native module or confirmed nested container' ), 'confirmed_parent_rules' => self::ai_parent_rules(), 'module_policy' => array( 'heading' => 'Use Heading for editable headings when its verified schema is available.', 'text' => 'Use Text for ordinary rich text only.', 'image' => 'Use Image for staged visual assets and ordinary images.', 'button' => 'Use Button for actions; do not imitate buttons inside Text.', 'blurb' => 'Use Blurb for icon, title, and description cards.', 'icon' => 'Use Icon for standalone pictograms.', 'toggle' => 'Use Toggle for expandable questions and answers.', 'group' => 'Use Group as a native container directly inside Column, Group, or Group Carousel. Never insert a Row or Column between Column and Group.', 'nested' => 'Use only confirmed parent-child pairs for Accordion, Contact Form, Counters, Group Carousel, Icon List, Post Filter, Pricing Tables, Slider, Social Media Follow, Tabs, Timeline, and Video Slider.', 'woocommerce'=> 'Use verified native WooCommerce modules only in the correct product, cart, checkout, account, or shop context.', ), 'design_policy' => array( 'use native background, typography, spacing, sizing, layout, border, shadow, transform, animation, and visibility settings', 'use desktop values by default and add tablet or phone values only when behavior changes', 'keep columns in a sensible mobile reading order', 'inherit destination fonts and ordinary colors unless the mockup requires an intentional override', 'never invent preset IDs, global colors, or global variables', ), 'image_policy' => array( 'use ordinary native modules for live text, buttons, icons, dividers, and simple cards', 'preserve indispensable device, product, illustration, or branded visual compositions through approved native Image modules', 'never use a complete page screenshot as the page layout', 'never fabricate an image URL; use only a trusted saved URL or a supplied staged asset token', 'when a person is composited over a designed environment, prefer separate background_plate and transparent_subject layers unless one flattened asset is intentionally better across all breakpoints', 'keep important marketing copy outside raster product previews so it stays editable', ), 'mockup_geometry_policy' => array( 'record section heights, maximum content width, column proportions, visual width and position, overlap start and end, field and button proportions, and major spacing anchors as approximate percentages', 'preserve intentional headline line breaks with separate Heading modules when lines need different typography or color', 'implement in fidelity order: section surfaces, hero composition and crops, typography, conversion elements, supporting patterns, decorative details, then breakpoint adaptations', ), 'responsive_policy' => array( 'desktop establishes the primary composition', 'tablet adapts multi-column spacing and widths without discarding hierarchy', 'phone stacks columns in a sensible reading order and uses explicit smaller type and spacing only where needed', 'major media must remain legible and buttons must remain easy to tap', 'phone is a deliberate recomposition with checked crop, stacking, wrapping, button width, clipping, and horizontal overflow', ), 'quality_gate' => array( 'every visible page-body section in the plan is represented', 'major visual assets are present in their intended sections', 'headings, body copy, and actions use semantic editable modules', 'palette, type scale, spacing rhythm, alignment, and visual alternation make the result recognizable', 'native validation passes before a proposal is staged', 'a separate browser-rendered mockup review must pass when visual references were supplied', 'composition, imagery, typography, conversion elements, design language, responsiveness, editability, and portability are independently checked', 'if composition, imagery, typography, or conversion structure fails, never describe the proposal as a close recreation', ), 'portability_policy' => array( 'use the verified direct Builder context et_builder and preserve its root envelope', 'leave presets null and global colors, global variables, canvases, images, and thumbnails empty unless verified portable definitions are intentionally included', 'keep page custom CSS empty', 'use the implicit default module preset unless a named destination-site preset and its exported definition are verified', 'direct Builder imports can replace the current page; validation, staging, exact approval, and a WordPress revision remain mandatory', ), 'evidence_manifest' => self::ai_evidence_manifest(), 'prohibited' => array( 'custom CSS', 'page CSS', 'inline style attributes', 'divi/code modules', 'script or style elements', 'custom HTML used to imitate native components', ), ); } /** * Load the actual packaged skill documents for one AI stage. * * Previous Builder AI builds only sent a condensed PHP summary while the * complete Markdown evidence was merely named in a manifest. Keep the * summary for machine-friendly rules, but also provide the authoritative * skill text that the original agent workflow reads before it acts. * * @param string $stage planning, section_generation, or review. * @return array */ public static function ai_skill_context( $stage = 'section_generation' ) { $stage = sanitize_key( (string) $stage ); $documents = array( 'skill' => 'assets/ai/native-layouts/methodology.md', 'native_design_policy' => 'assets/ai/native-layouts/references/native-design-policy.md', ); if ( 'review' !== $stage ) { $documents['direct_import_format'] = 'assets/ai/native-layouts/references/direct-import-format.md'; $documents['module_evidence'] = 'assets/ai/native-layouts/references/module-evidence.md'; $documents['portability_and_presets'] = 'assets/ai/native-layouts/references/portability-and-presets.md'; $documents['woocommerce_modules'] = 'assets/ai/native-layouts/references/woocommerce-modules.md'; } if ( in_array( $stage, array( 'planning', 'section_generation', 'review' ), true ) ) { $documents['mockup_reconstruction'] = 'assets/ai/native-layouts/references/mockup-reconstruction.md'; } $loaded = array(); foreach ( $documents as $name => $path ) { $text = self::read_text_asset( $path ); if ( '' === $text ) { continue; } $loaded[ $name ] = array( 'path' => $path, 'sha256' => hash( 'sha256', $text ), 'content'=> substr( $text, 0, self::MAX_SKILL_DOCUMENT_CHARS ), ); } return array( 'version' => self::METHODOLOGY_VERSION, 'stage' => $stage, 'authority' => 'These are the complete packaged Divi 5 Layouts instructions for this stage. Follow their workflow and treat verified exports as stronger evidence than assumptions.', 'documents' => $loaded, ); } /** Return every native block name confirmed by the packaged export catalog. */ public static function ai_supported_module_names() { $data = self::read_json_asset( 'assets/ai/module-catalog.json' ); if ( ! is_array( $data ) ) { return array(); } $names = array_values( array_filter( array_keys( $data ), static function ( $name ) { return is_string( $name ) && 0 === strpos( $name, 'divi/' ) && 'divi/code' !== $name; } ) ); sort( $names ); return $names; } /** Return the complete parent matrix ported from the packaged validator. */ public static function ai_parent_rules() { $content_parents = array( 'divi/column', 'divi/group' ); return array( 'divi/placeholder' => array( 'layout root' ), 'divi/section' => array( 'divi/placeholder' ), 'divi/row' => array( 'divi/section' ), 'divi/column' => array( 'divi/row' ), 'divi/group' => array( 'divi/column', 'divi/group', 'divi/group-carousel' ), 'divi/group-carousel' => $content_parents, 'divi/icon-list' => $content_parents, 'divi/icon-list-item' => array( 'divi/icon-list' ), 'divi/accordion' => $content_parents, 'divi/accordion-item' => array( 'divi/accordion' ), 'divi/video-slider' => $content_parents, 'divi/video-slider-item' => array( 'divi/video-slider' ), 'divi/timeline' => $content_parents, 'divi/timeline-item' => array( 'divi/timeline' ), 'divi/tabs' => $content_parents, 'divi/tab' => array( 'divi/tabs' ), 'divi/social-media-follow' => $content_parents, 'divi/social-media-follow-network' => array( 'divi/social-media-follow' ), 'divi/slider' => $content_parents, 'divi/slide' => array( 'divi/slider' ), 'divi/pricing-tables' => $content_parents, 'divi/pricing-table' => array( 'divi/pricing-tables' ), 'divi/post-filter' => $content_parents, 'divi/post-filter-item' => array( 'divi/post-filter' ), 'divi/contact-form' => $content_parents, 'divi/contact-field' => array( 'divi/contact-form' ), 'divi/counters' => $content_parents, 'divi/counter' => array( 'divi/counters' ), 'other confirmed leaf modules' => $content_parents, ); } /** Return the verified direct-Builder portability envelope and anchor content. */ public static function ai_portability_reference() { $anchor = self::read_json_asset( 'assets/ai/native-layouts/anchor-exports/simple-section.json' ); if ( ! is_array( $anchor ) ) { return array(); } return array( 'context' => isset( $anchor['context'] ) ? (string) $anchor['context'] : '', 'data' => isset( $anchor['data'] ) && is_array( $anchor['data'] ) ? $anchor['data'] : array(), 'presets' => $anchor['presets'] ?? null, 'global_colors' => isset( $anchor['global_colors'] ) && is_array( $anchor['global_colors'] ) ? $anchor['global_colors'] : array(), 'global_variables' => isset( $anchor['global_variables'] ) && is_array( $anchor['global_variables'] ) ? $anchor['global_variables'] : array(), 'page_settings_meta' => isset( $anchor['page_settings_meta'] ) && is_array( $anchor['page_settings_meta'] ) ? $anchor['page_settings_meta'] : array(), 'canvases' => isset( $anchor['canvases'] ) && is_array( $anchor['canvases'] ) ? $anchor['canvases'] : array(), 'images' => isset( $anchor['images'] ) && is_array( $anchor['images'] ) ? $anchor['images'] : array(), 'thumbnails' => isset( $anchor['thumbnails'] ) && is_array( $anchor['thumbnails'] ) ? $anchor['thumbnails'] : array(), ); } /** Return the verified WooCommerce context rules packaged with the skill. */ public static function ai_woocommerce_policy() { return array( 'builder_version' => '5.11.0', 'parent' => 'divi/column or divi/group', 'product_context' => 'Use product modules on a WooCommerce product or verified Theme Builder product template unless a confirmed schema intentionally selects another product.', 'cart_context' => 'Use cart modules only on the matching WooCommerce cart page or template.', 'checkout_context'=> 'Use checkout modules only on the matching WooCommerce checkout page or template.', 'dependencies' => 'Never invent product IDs, category IDs, account IDs, preset IDs, or other site-specific values.', 'content' => 'WooCommerce supplies much of the rendered content; an empty default inner-content shape is not necessarily an incomplete module.', 'accessibility' => 'Apply appearance through verified native Design attributes while preserving WooCommerce functionality and accessibility.', 'confirmed_modules' => array_values( array_filter( self::ai_supported_module_names(), static function ( $name ) { return 'divi/shop' === $name || 0 === strpos( $name, 'divi/woocommerce-' ); } ) ), ); } /** Report the packaged evidence used at runtime and retained for future audits. */ public static function ai_evidence_manifest() { return array( 'methodology' => 'assets/ai/native-layouts/methodology.md', 'runtime' => 'The complete applicable Markdown documents are loaded by ai_skill_context() and supplied to planning, section generation, and rendered review.', 'references' => array( 'native design policy', 'direct import format', 'module evidence and parent relationships', 'portability and presets', 'WooCommerce module contexts', 'high-fidelity mockup reconstruction', ), 'catalogs' => array( 'full module catalog', 'common module catalog' ), 'exports' => array( 'verified direct-Builder anchor', 'golden rendered layout', 'four dated native module exports including WooCommerce' ), 'validator' => 'The skill validator behavior is ported to the PHP layout validator; WordPress does not depend on Python.', ); } /** Read one trusted JSON file bundled with Divi Powers. */ private static function read_json_asset( $relative_path ) { $path = defined( 'DIVI_POWERS_PATH' ) ? DIVI_POWERS_PATH . ltrim( (string) $relative_path, '/' ) : ''; if ( '' === $path || ! is_readable( $path ) ) { return null; } $json = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $data = false !== $json ? json_decode( $json, true ) : null; return is_array( $data ) ? $data : null; } /** Read one trusted UTF-8 text asset bundled with Divi Powers. */ private static function read_text_asset( $relative_path ) { $path = defined( 'DIVI_POWERS_PATH' ) ? DIVI_POWERS_PATH . ltrim( (string) $relative_path, '/' ) : ''; if ( '' === $path || ! is_readable( $path ) || self::MAX_SKILL_DOCUMENT_CHARS * 4 < (int) filesize( $path ) ) { return ''; } $text = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents return false !== $text ? trim( (string) $text ) : ''; } /** * Return verified module examples selected from the packaged evidence catalog. * * @param array $module_names Requested native block names. * @return array */ public static function ai_module_catalog( $module_names = array() ) { $path = defined( 'DIVI_POWERS_PATH' ) ? DIVI_POWERS_PATH . 'assets/ai/module-catalog.json' : ''; if ( '' === $path || ! is_readable( $path ) ) { return array(); } $json = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $data = false !== $json ? json_decode( $json, true ) : null; if ( ! is_array( $data ) ) { return array(); } $defaults = array( 'divi/section', 'divi/row', 'divi/column', 'divi/heading', 'divi/text', 'divi/button', 'divi/image', 'divi/blurb', 'divi/divider', 'divi/icon', 'divi/group', 'divi/cta', 'divi/toggle', ); $requested = array_values( array_unique( array_merge( $defaults, array_map( 'sanitize_text_field', (array) $module_names ) ) ) ); $nested_pairs = array( 'divi/group-carousel' => 'divi/group', 'divi/icon-list' => 'divi/icon-list-item', 'divi/accordion' => 'divi/accordion-item', 'divi/video-slider' => 'divi/video-slider-item', 'divi/timeline' => 'divi/timeline-item', 'divi/tabs' => 'divi/tab', 'divi/social-media-follow' => 'divi/social-media-follow-network', 'divi/slider' => 'divi/slide', 'divi/pricing-tables' => 'divi/pricing-table', 'divi/post-filter' => 'divi/post-filter-item', 'divi/contact-form' => 'divi/contact-field', 'divi/counters' => 'divi/counter', ); foreach ( $nested_pairs as $container => $item ) { if ( in_array( $container, $requested, true ) || in_array( $item, $requested, true ) ) { $requested[] = $container; $requested[] = $item; } } $requested = array_values( array_unique( $requested ) ); $selected = array(); foreach ( $requested as $name ) { if ( isset( $data[ $name ] ) && is_array( $data[ $name ] ) ) { $selected[ $name ] = self::sanitize_ai_catalog_value( $data[ $name ] ); } } return $selected; } /** Return a verified, rendered native feature-grid block reference. */ public static function ai_golden_layout_reference() { $path = defined( 'DIVI_POWERS_PATH' ) ? DIVI_POWERS_PATH . 'assets/ai/feature-grid-golden.json' : ''; if ( '' === $path || ! is_readable( $path ) ) { return ''; } $json = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $data = false !== $json ? json_decode( $json, true ) : null; if ( ! is_array( $data ) || empty( $data['data'] ) || ! is_array( $data['data'] ) ) { return ''; } foreach ( $data['data'] as $content ) { if ( is_string( $content ) && false !== strpos( $content, '/', $content ); } /** Return whether the page still contains exactly the old generated wrapper. */ public static function is_legacy_wrapper( $content, $type ) { $content = trim( (string) $content ); $type = sanitize_key( (string) $type ); if ( '' === $content || '' === $type ) { return '' === $content; } $pattern = '/^\[divi_powers_starter_page\s+type=(?:"|\')?' . preg_quote( $type, '/' ) . '(?:"|\')?\s*\]$/i'; return 1 === preg_match( $pattern, $content ); } /** Generate the complete native Free starter page. */ public static function lite_demo() { $bundled = self::bundled_lite_demo(); if ( '' !== $bundled ) { return $bundled; } // Keep the former generated demo as a defensive fallback if the bundled // starter asset is unavailable or malformed. $blocks = array(); $blocks[] = self::section( self::row( self::column( self::text( '

DISMISSIBLE ELEMENT Close this announcement, refresh the page, and inspect this Section in Advanced → Divi Powers.

', array( 'dp-native-notice__content' ), array(), 'Demo 01 — Dismissible announcement text' ) ) ), array( 'dp-native-starter', 'dp-native-notice' ), array( 'enabled' => 'on', 'persistence' => 'session', 'position' => 'top-right', 'label' => 'Dismiss the Divi Powers demo announcement', ), 'Demo 01 — Dismissible Section' ); $hero = self::text( '

5D POWERS FOR DIVI FREE · SAMPLE PAGE

Try the features, then see how they work in Divi.

Every example uses editable Divi sections, rows, columns, text, and buttons with 5D Powers settings already configured.

', array( 'dp-native-hero__copy' ), array(), 'Demo introduction — inspect this Text module' ); $hero .= self::button( 'Open the modal', '#', array( 'dp-native-button', 'dp-native-button--primary' ), array( 'triggerClickAction' => 'show', 'triggerEvent' => 'click', 'triggerTargetPowerId' => 'dp-lite-modal', ), 'Demo 02 — Modal trigger' ); $hero .= self::button( 'Open the right drawer', '#', array( 'dp-native-button', 'dp-native-button--secondary' ), array( 'triggerClickAction' => 'show', 'triggerEvent' => 'click', 'triggerTargetPowerId' => 'dp-lite-drawer', ), 'Demo 03 — Drawer trigger' ); $hero .= self::button( 'Hover or focus for a tooltip', '#', array( 'dp-native-button', 'dp-native-button--ghost' ), array( 'triggerClickAction' => 'show', 'triggerEvent' => 'hover-focus', 'triggerTargetPowerId' => 'dp-lite-tooltip', ), 'Demo 04 — Tooltip trigger' ); $blocks[] = self::section( self::row( self::column( $hero, array( 'dp-native-hero__column' ) ), array( 'dp-native-hero__row' ) ), array( 'dp-native-starter', 'dp-native-hero' ), array(), '5D Powers for Divi Free sample page hero' ); $blocks[] = self::section( self::row( self::column( self::text( '

MODAL TARGET

This modal is a native Divi Section.

The Section has Power ID dp-lite-modal, Presentation Mode set to Modal, a backdrop, Escape closing, page-scroll locking, and contextual motion.

Inspect: Advanced → Divi Powers on this Section.

', array( 'dp-native-presentation__copy' ), array(), 'Demo 02 — Modal content' ) ) ), array( 'dp-native-presentation', 'dp-native-modal' ), array( 'triggerPowerId' => 'dp-lite-modal', 'modalPresentation' => 'modal', 'modalShowBackdrop' => 'on', 'modalCloseBackdrop' => 'on', 'modalCloseEscape' => 'on', 'modalShowCloseButton' => 'on', 'modalClosePosition' => 'top-right', 'modalDialogLabel' => 'Divi Powers modal demonstration', 'modalCloseLabel' => 'Close the Divi Powers modal demonstration', 'modalLockScroll' => 'on', 'modalAnimation' => 'fade-scale', ), 'Demo 02 — Native modal target' ); $blocks[] = self::section( self::row( self::column( self::text( '

RIGHT DRAWER TARGET

Drawers use the same trigger-and-target relationship.

Change Presentation Mode to Left Drawer, Bottom Sheet, Modal, Tooltip, or Popover to see how the same native Section can serve a different purpose.

', array( 'dp-native-presentation__copy' ), array(), 'Demo 03 — Drawer content' ) ) ), array( 'dp-native-presentation', 'dp-native-drawer' ), array( 'triggerPowerId' => 'dp-lite-drawer', 'modalPresentation' => 'drawer-right', 'modalShowBackdrop' => 'on', 'modalCloseBackdrop' => 'on', 'modalCloseEscape' => 'on', 'modalShowCloseButton' => 'on', 'modalClosePosition' => 'top-right', 'modalDialogLabel' => 'Divi Powers drawer demonstration', 'modalCloseLabel' => 'Close the Divi Powers drawer demonstration', 'modalLockScroll' => 'on', 'modalAnimation' => 'fade-scale', ), 'Demo 03 — Native right drawer target' ); $blocks[] = self::section( self::row( self::column( self::text( '

TOOLTIP TARGET
Tooltips and popovers are ordinary Divi modules anchored to their trigger.

', array( 'dp-native-tooltip' ), array( 'triggerPowerId' => 'dp-lite-tooltip', 'modalPresentation' => 'tooltip', 'floatingPlacement' => 'top', 'floatingAlignment' => 'center', 'floatingOffset' => '12', 'floatingShowArrow' => 'on', 'floatingOpenDelay' => '100', 'floatingCloseDelay' => '120', 'floatingHoverContent' => 'on', 'floatingCloseEscape' => 'on', 'floatingMaxWidth' => '320', ), 'Demo 04 — Native tooltip target' ) ) ), array( 'dp-native-tooltip-host' ), array(), 'Demo 04 — Tooltip host Section' ); $switcher = self::text( '

A/B CONTENT SWITCHER

One Button module controls two native Divi targets.

Click the button, then inspect its Divi Powers fields to see the two target IDs, default state, transition, remembered selection, labels, and active-state styling.

', array( 'dp-native-section-heading' ), array(), 'Demo 05 — Content Switcher instructions' ); $switcher .= self::button( 'Switch the content state', '#', array( 'dp-native-button', 'dp-native-button--primary', 'dp-native-switcher-control' ), array( 'contentToggleEnabled' => 'on', 'contentSwitcherMode' => 'toggle', 'contentToggleTargetA' => 'dp-lite-state-a', 'contentToggleTargetB' => 'dp-lite-state-b', 'contentToggleDefault' => 'a', 'contentToggleTransition' => 'fade-slide', 'contentToggleRemember' => 'session', 'contentToggleActiveState' => 'b', 'contentToggleLabelA' => 'Show build state', 'contentToggleLabelB' => 'Show strategy state', 'contentActiveStyleEnabled' => 'on', 'contentActiveTextColor' => '#ffffff', 'contentActiveBackground' => '#3157f4', 'contentActiveBorderColor' => '#3157f4', 'contentActiveBorderWidth' => '1', 'contentActiveBorderRadius' => '14', 'contentActiveFontWeight' => '800', 'contentActiveTransform' => 'lift', 'contentActiveTransition' => '180', ), 'Demo 05 — A/B Content Switcher control' ); $blocks[] = self::section( self::row( self::column( $switcher, array( 'dp-native-switcher__column' ) ) ), array( 'dp-native-starter', 'dp-native-switcher' ), array(), 'Demo 05 — Content Switcher section' ); $blocks[] = self::section( self::row( self::column( self::text( '

STATE A

Strategy first

Map the desired visitor action, assign clear Power IDs, and then connect each trigger to the target it controls.

', array( 'dp-native-state-card' ), array(), 'Demo 05 — Switcher state A content' ) ) ), array( 'dp-native-starter', 'dp-native-state', 'dp-native-state--a' ), array( 'triggerPowerId' => 'dp-lite-state-a', 'triggerTransition' => 'fade-slide', ), 'Demo 05 — Switcher target A' ); $blocks[] = self::section( self::row( self::column( self::text( '

STATE B

Build with native Divi elements

Apply the Power settings to existing modules instead of rebuilding the design in a proprietary popup or tab system.

', array( 'dp-native-state-card' ), array(), 'Demo 05 — Switcher state B content' ) ) ), array( 'dp-native-starter', 'dp-native-state', 'dp-native-state--b' ), array( 'triggerPowerId' => 'dp-lite-state-b', 'triggerTransition' => 'fade-slide', ), 'Demo 05 — Switcher target B' ); $nav = self::text( '

SCROLL NAVIGATION

These Button modules track native page Sections.

Each control uses Content Switcher → Scroll Navigation mode. Scroll the page and watch the active state move with the section currently in view.

', array( 'dp-native-section-heading' ), array(), 'Demo 06 — Scroll Navigation instructions' ); foreach ( array( 'one' => 'Dismiss', 'two' => 'Connect', 'three' => 'Switch' ) as $suffix => $label ) { $nav .= self::button( $label, '#', array( 'dp-native-button', 'dp-native-button--nav' ), array( 'contentToggleEnabled' => 'on', 'contentSwitcherMode' => 'scroll', 'scrollNavGroupId' => 'dp-lite-scroll-demo', 'scrollNavAction' => 'activate', 'scrollNavTargetPowerId' => 'dp-lite-scroll-' . $suffix, 'scrollNavBehavior' => 'smooth', 'scrollNavOffset' => '32', 'scrollNavActiveMode' => 'center', 'scrollNavHash' => 'on', 'scrollNavOpenHash' => 'on', 'scrollNavProgress' => 'both', 'scrollNavProgressLabel' => 'Section {current} of {total}: {label}', 'contentActiveStyleEnabled' => 'on', 'contentActiveTextColor' => '#ffffff', 'contentActiveBackground' => '#6c3ee8', 'contentActiveBorderColor' => '#6c3ee8', 'contentActiveBorderRadius' => '999', 'contentActiveFontWeight' => '800', ), 'Demo 06 — Scroll navigation: ' . $label ); } $blocks[] = self::section( self::row( self::column( $nav, array( 'dp-native-scroll-nav__column' ) ) ), array( 'dp-native-starter', 'dp-native-scroll-nav' ), array(), 'Demo 06 — Scroll Navigation controls' ); $scroll_sections = array( 'one' => array( 'Dismiss anything', 'Add remembered close behavior to announcements, promos, helper panels, and temporary notices.' ), 'two' => array( 'Connect anything', 'Link buttons, images, text, rows, columns, and sections using matching Power IDs.' ), 'three' => array( 'Switch anything', 'Coordinate A/B states, grouped panels, deep links, and active one-page navigation.' ), ); foreach ( $scroll_sections as $suffix => $copy ) { $blocks[] = self::section( self::row( self::column( self::text( '

SCROLL TARGET

' . esc_html( $copy[0] ) . '

' . esc_html( $copy[1] ) . '

Inspect: this Section has Power ID dp-lite-scroll-' . esc_html( $suffix ) . '.

', array( 'dp-native-scroll-target__copy' ), array(), 'Demo 06 — Scroll target ' . ucfirst( $suffix ) ) ) ), array( 'dp-native-starter', 'dp-native-scroll-target', 'dp-native-scroll-target--' . $suffix ), array( 'triggerPowerId' => 'dp-lite-scroll-' . $suffix ), 'Demo 06 — Native scroll target ' . ucfirst( $suffix ) ); } $blocks[] = self::section( self::row( self::column( self::text( '

YOUR TURN

Open this page in the Visual Builder.

Use Divi’s Layers panel and the descriptive admin labels to find each example. Open Advanced → Divi Powers, change a field, save the page, and test the result.

', array( 'dp-native-final__copy' ), array(), 'Final instructions — edit the native examples' ) ) ), array( 'dp-native-starter', 'dp-native-final' ), array(), 'Native demo final instructions' ); return self::placeholder( implode( "\n", $blocks ) ); } /** Load the tested, self-contained native Divi starter page bundled with Free. */ private static function bundled_lite_demo() { if ( ! defined( 'DIVI_POWERS_PATH' ) || ! defined( 'DIVI_POWERS_URL' ) ) { return ''; } $path = DIVI_POWERS_PATH . 'assets/layouts/lite-starter-use-case-tour.json'; if ( ! is_readable( $path ) ) { return ''; } $raw = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents if ( false === $raw || '' === trim( $raw ) ) { return ''; } $layout = json_decode( $raw, true ); if ( ! is_array( $layout ) || empty( $layout['data'] ) || ! is_array( $layout['data'] ) ) { return ''; } $content = reset( $layout['data'] ); $content = is_string( $content ) ? $content : ''; if ( '' === $content ) { return ''; } $image_url = esc_url_raw( DIVI_POWERS_URL . 'assets/images/starter-email-marketing.png' ); $content = str_replace( '{{DIVI_POWERS_STARTER_IMAGE_URL}}', $image_url, $content ); $content = str_replace( array( '/wp-admin/admin.php?page=divi-powers-design-library', '/wp-admin/admin.php?page=divi-powers-getting-started', ), array( esc_url_raw( admin_url( 'admin.php?page=divi-powers-design-library' ) ), esc_url_raw( admin_url( 'admin.php?page=divi-powers-getting-started' ) ), ), $content ); return self::is_native_divi_content( $content ) ? $content : ''; } /** Generate one native Pro funnel page. */ public static function pro_page( $type, $context = array() ) { $type = sanitize_key( (string) $type ); $context = is_array( $context ) ? $context : array(); $landing = isset( $context['landing_url'] ) ? (string) $context['landing_url'] : '#'; $offer = isset( $context['offer_url'] ) ? (string) $context['offer_url'] : '#'; $checkout= isset( $context['checkout_url'] ) ? (string) $context['checkout_url'] : '#'; if ( 'landing' === $type ) { $blocks = array(); $blocks[] = self::section( self::row( self::column( self::text( '

LIVE DIVI POWERS DEMO Dismiss this native Section and continue through the connected funnel.

', array( 'dp-native-notice__content' ), array(), 'Funnel landing — Dismissible notice text' ) ) ), array( 'dp-native-starter', 'dp-native-notice' ), array( 'enabled' => 'on', 'persistence' => 'session', 'position' => 'top-right', 'label' => 'Dismiss the funnel demo notice' ), 'Funnel landing — Dismissible Section' ); $hero = self::text( '

THE MARKETING & FUNNEL PLATFORM BUILT NATIVELY FOR DIVI

Turn ordinary Divi pages into a connected conversion journey.

This landing page, its modal, the next offer page, checkout page, and thank-you page are all native editable Divi layouts.

', array( 'dp-native-hero__copy' ), array(), 'Funnel landing — Hero copy' ); $hero .= self::button( 'Explore the free plugin', $offer, array( 'dp-native-button', 'dp-native-button--primary' ), array(), 'Funnel landing — Continue to offer' ); $hero .= self::button( 'See a Power in action', '#', array( 'dp-native-button', 'dp-native-button--secondary' ), array( 'triggerClickAction' => 'show', 'triggerEvent' => 'click', 'triggerTargetPowerId' => 'dp-pro-landing-modal' ), 'Funnel landing — Modal trigger' ); $blocks[] = self::section( self::row( self::column( $hero, array( 'dp-native-hero__column' ) ) ), array( 'dp-native-starter', 'dp-native-hero', 'dp-native-pro-landing' ), array(), 'Funnel landing — Native Divi hero' ); $cards = self::text( '

01 · DESIGN IN DIVI

Keep every page editable

Build the visual experience with normal Divi modules.

', array( 'dp-native-card' ), array(), 'Landing proof card — Design' ); $cards .= self::text( '

02 · CONNECT THE JOURNEY

Map the path

Preserve attribution, route checkout, and define conversion.

', array( 'dp-native-card' ), array(), 'Landing proof card — Connect' ); $cards .= self::text( '

03 · PROVE IT WORKS

Validate end to end

Inspect every milestone before sending paid traffic.

', array( 'dp-native-card' ), array(), 'Landing proof card — Validate' ); $blocks[] = self::section( self::row( self::column( $cards, array( 'dp-native-card-grid__column' ) ), array( 'dp-native-card-grid__row' ) ), array( 'dp-native-starter', 'dp-native-card-grid' ), array(), 'Funnel landing — Three proof cards' ); $blocks[] = self::section( self::row( self::column( self::text( '

TRIGGERS & TARGETS

This is a native Divi Section—not a separate popup builder.

The landing-page Button and this Section are connected through Power ID dp-pro-landing-modal.

', array( 'dp-native-presentation__copy' ), array(), 'Funnel landing — Modal content' ) ) ), array( 'dp-native-presentation', 'dp-native-modal' ), array( 'triggerPowerId' => 'dp-pro-landing-modal', 'modalPresentation' => 'modal', 'modalShowBackdrop' => 'on', 'modalCloseBackdrop' => 'on', 'modalCloseEscape' => 'on', 'modalShowCloseButton' => 'on', 'modalDialogLabel' => 'Divi Powers funnel modal', 'modalCloseLabel' => 'Close funnel modal', 'modalLockScroll' => 'on', 'modalAnimation' => 'fade-scale' ), 'Funnel landing — Native modal target' ); return self::placeholder( implode( "\n", $blocks ) ); } if ( 'offer' === $type ) { $copy = self::text( '

5D POWERS FOR DIVI FREE

Build your first sales funnel directly inside Divi.

Use editable Divi pages to guide visitors from your offer through WooCommerce checkout and a thank-you page.

  • Editable Divi pages
  • Unlimited basic funnels
  • Three checkout layouts
  • Basic funnel activity reporting
', array( 'dp-native-offer__copy' ), array(), 'Funnel offer — Main sales copy' ); $copy .= self::button( 'Get 5D Powers for Divi Free', $checkout, array( 'dp-native-button', 'dp-native-button--primary' ), array(), 'Funnel offer — Checkout CTA' ); $aside = self::text( '

WHAT THIS PAGE PROVES

The checkout can be several steps after the entry page.

Divi Powers preserves funnel and campaign context between normal WordPress pages, then attributes the order to the correct journey.

Landing → Offer → Checkout → Thank You

', array( 'dp-native-offer__aside' ), array(), 'Funnel offer — Journey explanation' ); return self::placeholder( self::section( self::row( self::column( self::button( '← Back to landing', $landing, array( 'dp-native-button', 'dp-native-button--link' ), array(), 'Funnel offer — Back link' ) . $copy, array( 'dp-native-offer__main' ), '14_24' ) . self::column( $aside, array( 'dp-native-offer__side' ), '10_24' ), array( 'dp-native-offer__row' ), 'equal-columns_2' ), array( 'dp-native-starter', 'dp-native-offer' ), array(), 'Funnel offer — Native Divi sales page' ) ); } if ( 'checkout' === $type ) { $header = self::text( '

SECURE TEST CHECKOUT

Complete the example journey.

WooCommerce owns the order and payment lifecycle. Divi Powers controls the journey, layout, attribution, and destination.

', array( 'dp-native-checkout__header' ), array(), 'Funnel checkout — Header' ); $checkout_module = self::text( '[woocommerce_checkout]', array( 'dp-native-shortcode', 'dp-native-checkout__renderer' ), array(), 'Funnel checkout — WooCommerce Checkout renderer' ); return self::placeholder( self::section( self::row( self::column( $header . $checkout_module ) ), array( 'dp-native-starter', 'dp-native-checkout' ), array(), 'Funnel checkout — Native Divi checkout page' ) ); } if ( 'thank-you' === $type ) { $content = self::text( '

✓

CUSTOM CONVERSION DESTINATION

The funnel reached its thank-you page.

This native Divi page is the conversion goal connected after checkout in the Funnel Builder.

', array( 'dp-native-thanks__header' ), array(), 'Funnel thank-you — Header' ); $content .= self::text( '[divi_powers_order_confirmation]', array( 'dp-native-shortcode', 'dp-native-thanks__confirmation' ), array(), 'Funnel thank-you — Verified order confirmation' ); $content .= self::button( 'Restart the example journey', $landing, array( 'dp-native-button', 'dp-native-button--secondary' ), array(), 'Funnel thank-you — Restart journey' ); return self::placeholder( self::section( self::row( self::column( $content ) ), array( 'dp-native-starter', 'dp-native-thanks' ), array(), 'Funnel thank-you — Native Divi conversion page' ) ); } return self::placeholder( self::section( self::row( self::column( self::text( '

Divi Powers starter page

', array(), array(), 'Divi Powers starter page' ) ) ) ) ); } private static function placeholder( $content ) { return '' . "\n" . trim( (string) $content ) . "\n" . ''; } private static function section( $inner, $classes = array(), $power = array(), $label = '' ) { $attrs = self::module_attrs( $classes, $label ); $attrs = self::with_power( $attrs, $power ); return self::container_block( 'divi/section', $attrs, $inner ); } private static function row( $inner, $classes = array(), $structure = 'equal-columns_1' ) { $attrs = self::module_attrs( $classes, '' ); $attrs['module']['advanced']['flexColumnStructure']['desktop']['value'] = sanitize_key( $structure ); $attrs['module']['decoration']['layout']['desktop']['value']['display'] = 'flex'; $attrs['module']['decoration']['layout']['desktop']['value']['flexWrap'] = 'nowrap'; $attrs['module']['decoration']['layout']['tablet']['value']['flexDirection'] = 'column'; $attrs['module']['decoration']['layout']['phone']['value']['flexDirection'] = 'column'; return self::container_block( 'divi/row', $attrs, $inner ); } private static function column( $inner, $classes = array(), $flex_type = '24_24' ) { $attrs = self::module_attrs( $classes, '' ); $attrs['module']['decoration']['sizing']['desktop']['value']['flexType'] = sanitize_text_field( $flex_type ); $attrs['module']['decoration']['sizing']['tablet']['value']['flexType'] = '24_24'; $attrs['module']['decoration']['sizing']['phone']['value']['flexType'] = '24_24'; return self::container_block( 'divi/column', $attrs, $inner ); } private static function text( $html, $classes = array(), $power = array(), $label = '' ) { $attrs = self::module_attrs( $classes, $label ); $attrs['module']['advanced']['text']['text']['desktop']['value']['orientation'] = 'left'; $attrs['content']['innerContent']['desktop']['value'] = self::normalize_text_markup( $html ); $attrs = self::with_power( $attrs, $power ); return self::leaf_block( 'divi/text', $attrs ); } /** * Divi 5 Text modules are most reliable when rich HTML has one root node. * Shortcodes remain unwrapped so their output continues to render normally. */ private static function normalize_text_markup( $html ) { $html = trim( (string) $html ); if ( '' === $html ) { return '

'; } if ( 1 === preg_match( '/^\[[^\r\n]+\]$/', $html ) ) { return $html; } if ( 1 === preg_match( '/^]*>.*<\/div>$/s', $html ) ) { return $html; } return '
' . $html . '
'; } private static function button( $text, $url, $classes = array(), $power = array(), $label = '' ) { $attrs = self::module_attrs( $classes, $label ); $attrs['button']['innerContent']['desktop']['value'] = array( 'text' => (string) $text, 'linkUrl' => esc_url_raw( (string) $url ), ); $attrs['button']['decoration']['button']['desktop']['value']['icon']['enable'] = 'off'; $attrs = self::with_power( $attrs, $power ); return self::leaf_block( 'divi/button', $attrs ); } private static function module_attrs( $classes = array(), $label = '' ) { $attrs = array( 'builderVersion' => self::BUILDER_VERSION, 'modulePreset' => array( 'default' ), ); $attrs['module']['decoration']['layout']['desktop']['value']['display'] = 'block'; $classes = array_values( array_filter( array_map( 'sanitize_html_class', (array) $classes ) ) ); if ( $classes ) { $value = implode( ' ', $classes ); $attrs['module']['decoration']['attributes']['desktop']['value']['attributes'] = array( array( 'id' => substr( md5( 'class|' . $value . '|' . $label ), 0, 13 ), 'name' => 'class', 'value' => $value, 'adminLabel' => 'CSS Class', 'targetElement' => '', ), ); } if ( '' !== trim( (string) $label ) ) { $attrs['module']['meta']['adminLabel']['desktop']['value'] = sanitize_text_field( (string) $label ); } return $attrs; } private static function with_power( $attrs, $power ) { $power = is_array( $power ) ? array_filter( $power, static function ( $value ) { return null !== $value && '' !== $value; } ) : array(); if ( $power ) { $attrs['diviPowersDismissible']['innerContent']['desktop']['value'] = $power; } return $attrs; } private static function container_block( $name, $attrs, $inner ) { return '' . "\n" . trim( (string) $inner ) . "\n" . ''; } private static function leaf_block( $name, $attrs ) { return ''; } private static function encode( $value ) { return wp_json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } }