GO ` * renders verbatim. `badge` is a custom element missing from WP's * `$allowedposttags`, so `wp_kses_post()` strips it; these fields must go * through this richer inline whitelist instead. * * @return array> */ function aksrt_icon_card_kses_tags(): array { return [ 'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true], 'badge' => ['class' => true], 'span' => ['class' => true], 'i' => ['class' => true, 'aria-hidden' => true], 'em' => ['class' => true], 'strong' => ['class' => true], 'b' => ['class' => true], 'small' => ['class' => true], 'br' => [], ]; } endif; if (!function_exists('aksrt_icon_card_kses')) : /** * KSES with the shared inline whitelist; falls back to wp_kses_post() * when the tag helper is unavailable (e.g. file not yet loaded). * * @param string $value Raw admin-authored markup. * @return string */ function aksrt_icon_card_kses(string $value): string { $tags = aksrt_icon_card_kses_tags(); return $tags ? wp_kses($value, $tags) : wp_kses_post($value); } endif; if (!function_exists('aksrt_card_icon_inner')) : /** * Resolve a card's stored `icon` value to renderable inner markup. * * The universal icon picker stores three formats: * - "fa fas fa-star" — FA5 class string * - "zibsvg-hot" — theme built-in SVG sprite key * - "customsvg-" — inline custom SVG markup * * Every format must be routed through aksrt_icon_markup(); emitting a bare * FA class string silently produced an empty glyph for the two SVG formats * (e.g. ``), so the card rendered without an * icon whenever the admin picked 内置 SVG or 自定义 SVG. * * Raw inline markup without a picker prefix is still accepted for data * stored before the picker existed. * * @param string $value Stored icon value. * @param string $extra Extra classes for the rendered element. * @return string */ function aksrt_card_icon_inner(string $value, string $extra = ''): string { $value = trim($value); if ($value === '') { return ''; } $is_picker_svg = str_starts_with($value, 'zibsvg-') || str_starts_with($value, 'customsvg-'); if (!$is_picker_svg && str_contains($value, '<')) { $allowed = function_exists('aksrt_svg_kses_tags') ? aksrt_svg_kses_tags() : []; $allowed['i'] = ['class' => true, 'aria-hidden' => true]; $allowed['span'] = ['class' => true]; return wp_kses($value, $allowed); } // Bare glyph keys ("gem", "fa-gem") predate the picker; promote them to a // full FA5 solid class, the same contract the picker emits. if (!$is_picker_svg && !str_contains($value, ' ')) { $value = 'fa fas ' . (str_starts_with($value, 'fa-') ? $value : 'fa-' . $value); } if (function_exists('aksrt_icon_markup')) { return aksrt_icon_markup($value, $extra); } return ''; } endif; if (!function_exists('aksrt_icon_card')) : /** * Vertical icon card (icon above title/desc). * * @param array $args * @param bool $echo */ function aksrt_icon_card(array $args = [], bool $echo = false): string { $defaults = [ 'type' => '', 'class' => 'box-body nopw-sm', 'icon' => '', 'icon_size' => 0, 'customize_icon' => '', 'icon_image' => '', 'link' => [ 'url' => '', 'target' => '', ], 'icon_radius' => false, 'icon_color' => '', 'icon_custom_color' => '', 'icon_class' => '', 'title' => '', 'desc' => '', ]; $args = wp_parse_args($args, $defaults); // Keep picker payloads intact (customsvg- carries inline SVG markup that // sanitize_text_field would strip down to a bare prefix). $icon_fa = function_exists('aksrt_sanitize_icon_value') ? aksrt_sanitize_icon_value((string) $args['icon']) : sanitize_text_field((string) $args['icon']); $icon_img = esc_url_raw(trim((string) ($args['icon_image'] !== '' ? $args['icon_image'] : $args['customize_icon']))); // customize_icon may be raw HTML (img) from older data. $custom_html = ''; if ($icon_img === '' && is_string($args['customize_icon']) && str_contains((string) $args['customize_icon'], '<')) { $custom_html = wp_kses((string) $args['customize_icon'], [ 'i' => ['class' => true, 'aria-hidden' => true], 'img' => ['src' => true, 'data-src' => true, 'alt' => true, 'class' => true, 'width' => true, 'height' => true], 'span'=> ['class' => true], ]); } if ($icon_fa === '' && $icon_img === '' && $custom_html === '') { return ''; } $link = $args['link']; if (is_string($link)) { $link = ['url' => $link, 'target' => '']; } if (!is_array($link)) { $link = ['url' => '', 'target' => '']; } $url = esc_url(trim((string) ($link['url'] ?? ''))); $target = sanitize_text_field((string) ($link['target'] ?? '')); $tgt = $target !== '' ? ' target="' . esc_attr($target) . '" rel="noopener noreferrer"' : ''; $icon_color = sanitize_html_class((string) $args['icon_color']); $icon_class_extra = sanitize_html_class((string) $args['icon_class']); $radius = !empty($args['icon_radius']); $icon_class = $radius ? 'card-icon toggle-radius fa-3x ' . ($icon_class_extra !== '' ? $icon_class_extra : $icon_color) : 'card-icon fa-4x ' . $icon_color; if ($icon_class_extra !== '' && !$radius) { $icon_class .= ' ' . $icon_class_extra; } $style = ''; $custom_color = sanitize_hex_color((string) $args['icon_custom_color']); if (!$custom_color && preg_match('/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/', (string) $args['icon_custom_color'])) { $custom_color = (string) $args['icon_custom_color']; } if (!$radius && $custom_color) { $style = ' style="color:' . esc_attr($custom_color) . ';"'; } if ($custom_html !== '') { $icon_inner = $custom_html; } elseif ($icon_img !== '') { $icon_inner = ''; } else { $icon_inner = aksrt_card_icon_inner($icon_fa); } $icon = '' . $icon_inner . ''; if ($url !== '') { $icon = '' . $icon . ''; } $icon_size = (int) $args['icon_size']; if ($icon_size > 0) { $icon = '' . $icon . ''; } $title = trim((string) $args['title']); $desc = trim((string) $args['desc']); if ($url !== '' && $title !== '') { $title_html = '' . aksrt_icon_card_kses($title) . ''; } else { $title_html = $title !== '' ? aksrt_icon_card_kses($title) : ''; } $body = ''; if ($title_html !== '') { $body .= '
' . $title_html . '
'; } if ($desc !== '') { $body .= '
' . aksrt_icon_card_kses($desc) . '
'; } $class = trim((string) $args['class'] . ' ' . (string) $args['type']); $html = '
'; $html .= $icon; $html .= $body !== '' ? '
' . $body . '
' : ''; $html .= '
'; if ($echo) { echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return ''; } return $html; } endif; if (!function_exists('aksrt_icon_cover_card')) : /** * Horizontal icon cover card (icon left, title/desc right). * * @param array $args * @param bool $echo */ function aksrt_icon_cover_card(array $args = [], bool $echo = false): string { $defaults = [ 'class' => 'aksrt-panel', 'icon' => '', 'icon_size' => 25, 'customize_icon' => '', 'custom_icon' => '', 'icon_image' => '', 'link' => [ 'url' => '', 'target' => '', ], // Flat keys used by header_components_icon_cards admin schema. 'url' => '', 'target' => '', 'icon_radius' => false, 'icon_color' => '', 'icon_custom_color' => '', 'icon_class' => '', 'title' => '', 'desc' => '', ]; $args = wp_parse_args($args, $defaults); // Normalize link: accept nested link[] or flat url/target (SPA schema). $link = $args['link']; if (is_string($link)) { $link = ['url' => $link, 'target' => (string) $args['target']]; } if (!is_array($link)) { $link = ['url' => '', 'target' => '']; } if (trim((string) ($link['url'] ?? '')) === '' && trim((string) $args['url']) !== '') { $link['url'] = (string) $args['url']; } if (trim((string) ($link['target'] ?? '')) === '' && trim((string) $args['target']) !== '') { $link['target'] = (string) $args['target']; } $icon_fa = trim((string) $args['icon']); // custom_icon (SPA) + customize_icon (legacy) + icon_image URL. $custom_src = (string) ($args['custom_icon'] !== '' ? $args['custom_icon'] : $args['customize_icon']); $icon_img = esc_url_raw(trim((string) ($args['icon_image'] !== '' ? $args['icon_image'] : ''))); $custom_html = ''; $custom_is_picker = str_starts_with($custom_src, 'zibsvg-') || str_starts_with($custom_src, 'customsvg-'); if ($icon_img === '' && $custom_is_picker) { // A picker value routed into the custom slot must go through the // shared renderer, otherwise the "customsvg-" prefix leaks as text. $custom_html = aksrt_card_icon_inner($custom_src); } elseif ($icon_img === '' && $custom_src !== '' && str_contains($custom_src, '<')) { $custom_html = wp_kses($custom_src, [ 'i' => ['class' => true, 'aria-hidden' => true], 'img' => ['src' => true, 'data-src' => true, 'alt' => true, 'class' => true, 'width' => true, 'height' => true], 'span' => ['class' => true], 'svg' => ['class' => true, 'viewbox' => true, 'viewBox' => true, 'width' => true, 'height' => true, 'fill' => true, 'xmlns' => true, 'aria-hidden' => true], 'path' => ['d' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true], 'use' => ['href' => true, 'xlink:href' => true], ]); } elseif ($icon_img === '' && $custom_src !== '' && filter_var($custom_src, FILTER_VALIDATE_URL)) { $icon_img = esc_url_raw($custom_src); } if ($icon_fa === '' && $icon_img === '' && $custom_html === '') { return ''; } $url = esc_url(trim((string) ($link['url'] ?? ''))); $target = sanitize_text_field((string) ($link['target'] ?? '')); // legacy only adds target= when non-empty; keep same for 1:1. $tgt = ($target !== '' && $target !== '_self') ? ' target="' . esc_attr($target) . '"' : ''; // legacy: class = "icon-cover-icon badg cir" + optional icon_class (may include radius4 / c-* / jb-*). // Do NOT sanitize_html_class on the whole string — it collapses multi-token classes. $icon_class_extra = trim(preg_replace('/\s+/', ' ', (string) $args['icon_class'])); $icon_class = 'icon-cover-icon badg cir'; if ($icon_class_extra !== '') { $tokens = preg_split('/\s+/', $icon_class_extra) ?: []; $safe = []; foreach ($tokens as $tok) { $tok = preg_replace('/[^a-zA-Z0-9_-]/', '', $tok); if ($tok !== '') { $safe[] = $tok; } } if ($safe) { $icon_class .= ' ' . implode(' ', $safe); } } if (!empty($args['icon_radius']) && !str_contains($icon_class, 'radius4')) { $icon_class .= ' radius4'; } $icon_color = trim((string) $args['icon_color']); if ($icon_color !== '' && !str_contains($icon_class, $icon_color)) { $icon_class .= ' ' . preg_replace('/[^a-zA-Z0-9_-]/', '', $icon_color); } $icon_size = max(14, min(64, (int) $args['icon_size'])); // legacy always sets font-size on the icon badge. $style = 'font-size: ' . $icon_size . 'px;'; $custom_color = sanitize_hex_color((string) $args['icon_custom_color']); if ($custom_color) { $style .= 'color:' . $custom_color . ';'; } if ($custom_html !== '') { $icon_inner = $custom_html; } elseif ($icon_img !== '') { $icon_inner = ''; } else { // FA5 class string / built-in SVG sprite key / inline custom SVG — // all three admin picker formats must survive this branch. $icon_inner = aksrt_card_icon_inner($icon_fa, 'em09'); } $icon = '
' . $icon_inner . '
'; // legacy stores/echoes title/desc raw (admin authors badge markup // such as GO), so these fields go // through the shared inline whitelist instead of wp_kses_post(). $title = (string) $args['title']; $desc = (string) $args['desc']; $text = ''; if (trim(wp_strip_all_tags($title)) !== '' || str_contains($title, '<')) { $text .= '
' . aksrt_icon_card_kses($title) . '
'; } if (trim(wp_strip_all_tags($desc)) !== '' || str_contains($desc, '<')) { $text .= '
' . aksrt_icon_card_kses($desc) . '
'; } $html = ''; if ($url !== '') { // legacy: $html .= ''; } // Keep every horizontal icon card on the same compact visual contract, // regardless of the page-specific wrapper that renders it. $html .= '
'; $html .= $icon; $html .= $text !== '' ? '
' . $text . '
' : ''; $html .= '
'; if ($url !== '') { $html .= '
'; } if ($echo) { echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return ''; } return $html; } endif; if (!function_exists('aksrt_obs_animate_attrs')) : /** * Build class + attribute string for scroll-in animation (maps to data-aksrt-animate). * * @param string $animation fade|slideup|slidedown|slideright|slideleft|zoomin|zoomout|'' * @param bool $repeat * @param float|int $delay_s * @return array{class:string,attr:string} */ function aksrt_obs_animate_attrs(string $animation, bool $repeat = false, $delay_s = 0): array { $animation = sanitize_key($animation); $map = [ 'fade' => 'fadeIn', 'slideup' => 'fadeInUp', 'slidedown' => 'fadeInDown', 'slideright' => 'fadeInRight', 'slideleft' => 'fadeInLeft', 'zoomin' => 'zoomIn', 'zoomout' => 'fadeIn', 'fadein' => 'fadeIn', 'fadeinup' => 'fadeInUp', ]; if ($animation === '' || !isset($map[$animation]) && !in_array($animation, $map, true)) { if (!isset($map[$animation])) { return ['class' => '', 'attr' => '']; } } $aksrt = $map[$animation] ?? $animation; $delay_ms = (int) round(((float) $delay_s) * 1000); $class = ' obs-animate ani-' . $animation; $attr = ' data-aksrt-animate="' . esc_attr($aksrt) . '"'; if ($delay_ms > 0) { $attr .= ' data-aksrt-animate-delay="' . esc_attr((string) $delay_ms) . '"'; } if ($repeat) { $attr .= ' data-animation-repeat="true"'; } return ['class' => $class, 'attr' => $attr]; } endif;