base_path = plugin_dir_path( __FILE__ ); $this->base_url = plugin_dir_url( __FILE__ ); // Normalize paths for consistent handling across operating systems. $wp_content_dir_normalized = wp_normalize_path( WP_CONTENT_DIR ); $base_path_normalized = wp_normalize_path( $this->base_path ); // Preparing icon base path with respect to wp content directory. $this->icon_base_path = str_replace( $wp_content_dir_normalized, '', $base_path_normalized . 'icons/' ); // Preparing image base path with respect to wp content directory. $this->img_base_path = str_replace( $wp_content_dir_normalized, '', $base_path_normalized . 'images/' ); include_once $this->base_path . 'classes/class-wt-ds-template-engine.php'; $this->prefix = $prefix ? $prefix : $this->prefix; // This is for plugins to use custom prefixes. $this->version = $version; // Plugin version. $this->load_config(); // Read the json file and store it as an array. // Alter kses allowed. add_filter( 'wp_kses_allowed_html', array( $this, 'kses_allowed_html' ), 10, 2 ); // Load the CSS and JS. add_action( 'admin_enqueue_scripts', array( $this, 'load_styles_and_scripts' ), 10 ); $this->engine = new Template_Engine( $this->prefix ); // Initiate the template parsing class. } /** * Get class instance. * * @param string $prefix Plugin prefix(Optional). */ public static function get_instance( $prefix = '' ) { self::$instance = is_null( self::$instance ) ? new Wbte_Ds( $prefix ) : self::$instance; return self::$instance; } /** * Read the config JSON and store it as class variable. */ protected function load_config() { $config_file = $this->base_path . 'wbte-ds-config.json'; if ( file_exists( $config_file ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $contents = file_get_contents( $config_file ); $this->config = $this->is_valid_json( $contents ) ? json_decode( $contents, true ) : array(); } } /** * Enqueue CSS and JS. * * @access public */ public function load_styles_and_scripts() { wp_enqueue_style( $this->prefix . 'ds_css', $this->base_url . 'css/style.css', array(), $this->version, 'all' ); wp_enqueue_script( $this->prefix . 'ds_js', $this->base_url . 'js/script.js', array( 'jquery' ), $this->version, false ); // Localize the script. $params = array( 'icon_base_url' => esc_url( WP_CONTENT_URL . $this->icon_base_path ), 'img_base_url' => esc_url( WP_CONTENT_URL . $this->img_base_path ), ); wp_localize_script( $this->prefix . 'ds_js', $this->prefix . 'ds_js_params', $params ); } /** * Checks the current JSON string is valid. * * @param string $json JSON string. */ protected function is_valid_json( $json ) { if ( function_exists( 'json_validate' ) ) { // For PHP 8.3+. return json_validate( $json ); } else { json_decode( $json ); return ( json_last_error() === JSON_ERROR_NONE ); } } /** * Fetch component template HTML from template file. * * @param string $slug Component slug. */ protected function fetch_template_html( $slug ) { $file_path = $this->base_path . 'views/' . $slug . '.html'; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents return file_exists( $file_path ) ? file_get_contents( $file_path ) : ''; } /** * Prepare variation component slug. * * @param string $parent_slug Parent component slug. * @param string $slug Component slug. */ public function prepare_variation_slug( $parent_slug, $slug ) { return $parent_slug . '-' . $slug; } /** * Get the component HTML. * * @param string $slug_str Component slug. If multiple slugs, separate by space. First one will be the parent and others are its attributes. * @param array $args Array of arguments. * @param object $engine Template engine object(Optional). * @return string Component HTML or empty string. */ public function get_component( $slug_str, $args, $engine = null ) { $html = ''; $slugs = array(); $slug_arr = explode( ' ', $slug_str ); // If multiple slugs. Assuming the first one will be the parent and others are its attributes. $parent_slug = array_shift( $slug_arr ); $components_config = isset( $this->config['components'] ) && is_array( $this->config['components'] ) ? $this->config['components'] : array(); if ( ! empty( $components_config[ $parent_slug ] ) ) { // Component found. $slugs[] = $parent_slug; $parent_comp = $components_config[ $parent_slug ]; $html = $this->fetch_template_html( $parent_slug ); $attributes = ! empty( $parent_comp['attributes'] ) ? $parent_comp['attributes'] : array(); foreach ( $slug_arr as $slug ) { // Prepare the attribute slug. $attr_slug = $this->prepare_variation_slug( $parent_slug, $slug ); if ( ! empty( $attributes[ $attr_slug ] ) ) { // Attribute found. $slugs[] = $attr_slug; $attr_data = $attributes[ $attr_slug ]; $attr_html = $this->fetch_template_html( $attr_slug ); // Replace or append the HTML. if ( ! empty( $attr_data['replace_html'] ) && ! empty( $attr_html ) ) { $html = $attr_html; // Replace the HTML. } else { $html .= $attr_html; // Append the HTML. } } } // Generate HTML for pagination component. if ( 'pagination' === $parent_slug ) { $pagination_comp_html_args = isset( $args['values'] ) && is_array( $args['values'] ) ? $args['values'] : array(); $pagination_comp_html_args['html'] = $html; $html = $this->prepare_pagination_component_html( $pagination_comp_html_args ); } $args['html'] = $html; $args['variations'] = $slugs; $args['parent_obj'] = $this; $args['icon_base'] = $this->icon_base_path; $args['img_base'] = $this->img_base_path; // Separate template engine object will be provided when component is rendered via component placeholder. This is to isolate the component from its parent component. $html = is_null( $engine ) ? $this->engine->render( $args ) : $engine->render( $args ); return $html; } return ''; } /** * Get absolute URL of assets like icons, images. * This will be useful when using DS assets directly. * * @param array $args Array of arguments. * @return string Absolute URL of the asset. Empty string if the file not found. */ public function get_asset( $args ) { $type = isset( $args['type'] ) ? $args['type'] : ''; $name = isset( $args['name'] ) ? $args['name'] : ''; if ( 'icon' === $type && file_exists( WP_CONTENT_DIR . $this->icon_base_path . $name . '.svg' ) ) { return WP_CONTENT_URL . $this->icon_base_path . $name . '.svg'; } elseif ( 'image' === $type && file_exists( WP_CONTENT_DIR . $this->img_base_path . $name ) ) { return WP_CONTENT_URL . $this->img_base_path . $name; } return ''; } /** * Get the component from component placeholders. * This is using in inner component situations. * * @param array $args Array of arguments. * @return string Component HTML or empty string. */ public function render_component_placeholder( $args ) { $html = ''; $slugs = isset( $args['slug'] ) && is_array( $args['slug'] ) ? $args['slug'] : array(); if ( ! empty( $slugs ) ) { $slug_str = implode( ' ', $slugs ); unset( $args['slug'] ); // This is to isolate the current component from other components. $engine = new Template_Engine( $this->prefix ); $html = $this->get_component( $slug_str, $args, $engine ); unset( $engine ); } return $html; } /** * Prepare the HTML for pagination component. * * @param array $args The arguments for preparing the pagination component HTML. * @return string The HTML for the pagination component. */ private function prepare_pagination_component_html( $args ) { $total = $args['total'] ?? 0; $limit = $args['limit'] ?? 0; $mxnav = $args['max_nav'] ?? 5; $crpage = $args['current_page'] ?? 1; $html = $args['html'] ?? ''; $url = $args['url'] ?? ''; if ( $total <= 0 || $limit <= 0 ) { return ''; } if ( isset( $args['offset'] ) ) { // Taking current page. $offset = $args['offset']; $crpage = floor( ( $offset + $limit ) / $limit ); $url_param = '{offset}'; } else { $url_param = '{pagenum}'; } $limit = $limit <= 0 ? 1 : $limit; $ttpg = ceil( $total / $limit ); if ( $ttpg < $crpage ) { return ''; } // Pagination calculations. $mxnav = $ttpg < $mxnav ? $ttpg : $mxnav; $mxnav_mid = floor( $mxnav / 2 ); $pgstart = $mxnav_mid >= $crpage ? 1 : $crpage - $mxnav_mid; $mxnav_mid += $mxnav_mid >= $crpage ? ( $mxnav_mid - $crpage ) : 0; // Adjusting the other half with the first half balance. $pgend = $crpage + $mxnav_mid; if ( $pgend > $ttpg ) { $pgend = $ttpg; } $find_replace = array(); // Pagination attributes. $pagination_attr = ' data-offset="{offset}" data-pagenum="{pagenum}"' . ( $url ? ' href="' . esc_url( $url ) . $url_param . '"' : '' ); // Previous button. $prev_offset_attr = ''; if ( $crpage > 1 ) { $offset = ( ( $crpage - 2 ) * $limit ); $prev_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( $offset, ( $crpage - 1 ) ), $pagination_attr ); } $find_replace['{{prev_button}}'] = '{{wbte-ds-icon-left-arrow-2}}'; // First page. if ( 1 < $pgstart ) { $page_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( 0, 1 ), $pagination_attr ); $find_replace['{{first_page}}'] = '1'; } // First dots. if ( 2 < $pgstart ) { $find_replace['{{first_dots}}'] = '...'; } // Menu numbers. $find_replace['{{numbers}}'] = ''; for ( $i = $pgstart; $i <= $pgend; $i++ ) { $i = (int) $i; $page_offset = ''; $page_offset_attr = ''; $offset = ( $i * $limit ) - $limit; if ( $i !== $crpage ) { $page_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( $offset, $i ), $pagination_attr ); } $find_replace['{{numbers}}'] .= '' . $i . ''; } // Last dots. if ( $pgend <= ( $ttpg - 2 ) ) { $find_replace['{{last_dots}}'] = '...'; } // Last page. if ( $pgend < $ttpg ) { $page_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( ( ( $ttpg - 1 ) * $limit ), $ttpg ), $pagination_attr ); $find_replace['{{last_page}}'] = '' . $ttpg . ''; } // Next button. $next_offset_attr = ''; if ( $crpage < $ttpg ) { $offset = ( $crpage * $limit ); $next_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( $offset, ( $crpage + 1 ) ), $pagination_attr ); } $find_replace['{{next_button}}'] = '{{wbte-ds-icon-right-arrow-3}}'; return str_replace( array_keys( $find_replace ), array_values( $find_replace ), $html ); } /** * Render the HTML. * * @param array $args Array of arguments. * @return string Rendered HTML. */ public function render_html( $args ) { $args = is_array( $args ) ? $args : array(); $args['html'] = $args['html'] ?? ''; $args['variations'] = $args['variations'] ?? array(); $args['parent_obj'] = $this; $args['icon_base'] = $this->icon_base_path; $args['img_base'] = $this->img_base_path; return $this->engine->render( $args ); } /** * Alter kses allowed HTML. * * @param array $allowedposttags Allowed tags. * @param string $context Context. * @return array Allowed tags. */ public function kses_allowed_html( $allowedposttags, $context ) { if ( 'post' === $context ) { // Define SVG tags and their safer allowed attributes. $svg_tags = array( 'svg' => array( 'xmlns' => true, 'width' => true, 'height' => true, 'viewBox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'preserveAspectRatio' => true, ), 'g' => array( 'fill' => true, 'stroke' => true, 'stroke-width' => true, ), 'path' => array( 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, ), 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, ), 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, ), 'line' => array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'stroke' => true, 'stroke-width' => true, ), 'polygon' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, ), 'polyline' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, ), 'text' => array( 'x' => true, 'y' => true, 'dx' => true, 'dy' => true, 'text-anchor' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, ), ); $allowedposttags = array_merge( $allowedposttags, $svg_tags ); } return $allowedposttags; } }