inner SVG), loaded once per request. * * @var array|null */ private static $set = null; /** * Path of the bundled icon set (generated by scripts/build-icons.js). * * @return string */ public static function file() { return KDOCS_PATH . 'assets/icons.json'; } /** * Bundled icons. * * @return array Name => inner SVG markup. */ public static function set() { if ( null === self::$set ) { self::$set = array(); $file = self::file(); $data = is_readable( $file ) ? json_decode( (string) file_get_contents( $file ), true ) : null; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents foreach ( ( is_array( $data ) && isset( $data['icons'] ) ) ? $data['icons'] : array() as $icon ) { self::$set[ $icon[0] ] = $icon[1]; } } return self::$set; } /** * Whether a bundled icon exists. * * @param string $name Name. * @return bool */ public static function exists( $name ) { return isset( self::set()[ (string) $name ] ); } /** * Reads a stored icon (JSON or legacy string) into the icon model. * * @param mixed $value Stored value. * @return array|null Icon, or null when empty. */ public static function parse( $value ) { if ( is_array( $value ) ) { $icon = $value; } else { $value = trim( (string) $value ); if ( '' === $value ) { return null; } $icon = '{' === $value[0] ? json_decode( $value, true ) : null; if ( ! is_array( $icon ) ) { if ( preg_match( '#^https?://#i', $value ) ) { $icon = array( 'type' => 'url', 'value' => $value, ); } elseif ( 0 === strpos( $value, 'dashicons-' ) ) { $icon = array( 'type' => 'dashicon', 'value' => $value, ); } else { $icon = array( 'type' => 'emoji', 'value' => $value, ); } } } return empty( $icon['type'] ) ? null : $icon; } /** * Validates an icon coming from the admin and returns the string to store. * * @param mixed $value JSON string, legacy string or icon array. * @return string Stored value ('' clears the icon). */ public static function sanitize( $value ) { $icon = self::parse( $value ); if ( ! $icon ) { return ''; } $clean = array( 'type' => $icon['type'] ); switch ( $icon['type'] ) { case 'icon': $name = sanitize_key( $icon['value'] ?? '' ); if ( ! self::exists( $name ) ) { return ''; } $clean['value'] = $name; $color = sanitize_hex_color( $icon['color'] ?? '' ); if ( $color ) { $clean['color'] = $color; } if ( isset( $icon['bg'] ) && in_array( $icon['bg'], self::BACKGROUNDS, true ) && 'none' !== $icon['bg'] ) { $clean['bg'] = $icon['bg']; } break; case 'emoji': // A short glyph: an emoji (possibly with modifiers) or a couple of characters. $emoji = wp_strip_all_tags( (string) ( $icon['value'] ?? '' ) ); $emoji = function_exists( 'mb_substr' ) ? mb_substr( $emoji, 0, 16 ) : substr( $emoji, 0, 32 ); if ( '' === trim( $emoji ) ) { return ''; } $clean['value'] = $emoji; break; case 'image': case 'svg': $id = absint( $icon['id'] ?? 0 ); if ( ! $id || 'attachment' !== get_post_type( $id ) ) { return ''; } $is_svg = 'image/svg+xml' === get_post_mime_type( $id ); if ( ! $is_svg && ! wp_attachment_is_image( $id ) ) { return ''; } $clean['type'] = $is_svg ? 'svg' : 'image'; $clean['id'] = $id; $clean['url'] = (string) wp_get_attachment_url( $id ); break; case 'url': $url = esc_url_raw( (string) ( $icon['value'] ?? '' ), array( 'http', 'https' ) ); if ( ! $url ) { return ''; } $clean['value'] = $url; break; case 'dashicon': $clean['value'] = sanitize_html_class( (string) ( $icon['value'] ?? '' ) ); break; default: return ''; } // Legacy plain strings stay plain strings: nothing to gain from re-encoding them. if ( in_array( $clean['type'], array( 'emoji', 'url', 'dashicon' ), true ) ) { return $clean['value']; } // No escaped slashes or unicode: update_metadata() unslashes values before storing. return (string) wp_json_encode( $clean, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } /** * Inline SVG of a bundled icon. * * @param string $name Icon name. * @param string $class Extra class. * @return string */ public static function svg( $name, $class = '' ) { $set = self::set(); if ( ! isset( $set[ $name ] ) ) { return ''; } return ''; } /** * Icon markup for the frontend. * * @param mixed $value Stored icon. * @param string $fallback Built-in fallback (doc|folder|kb), or '' for none. * @return string HTML. */ public static function render( $value, $fallback = 'doc' ) { $icon = self::parse( $value ); $html = $icon ? self::markup( $icon ) : ''; if ( '' === $html ) { return $fallback ? Frontend\View::svg( $fallback ) : ''; } return $html; } /** * Markup for a parsed icon. * * @param array $icon Icon. * @return string */ private static function markup( array $icon ) { switch ( $icon['type'] ) { case 'icon': $svg = self::svg( (string) ( $icon['value'] ?? '' ) ); if ( ! $svg ) { return ''; } $bg = isset( $icon['bg'] ) && in_array( $icon['bg'], self::BACKGROUNDS, true ) ? $icon['bg'] : 'none'; $color = sanitize_hex_color( $icon['color'] ?? '' ); $style = $color ? ' style="--kdocs-ico:' . esc_attr( $color ) . '"' : ''; return '' . $svg . ''; case 'emoji': return ''; case 'svg': $inline = Svg::inline( (int) ( $icon['id'] ?? 0 ) ); if ( $inline ) { return '' . $inline . ''; } // Falls through to an when the file can't be inlined. case 'image': $id = (int) ( $icon['id'] ?? 0 ); if ( ! $id || ! wp_get_attachment_url( $id ) ) { return ''; } return wp_get_attachment_image( $id, 'thumbnail', false, array( 'class' => 'kdocs-icon-img', 'alt' => '', 'loading' => 'lazy', 'decoding' => 'async', ) ); case 'url': return ''; case 'dashicon': return ''; } return ''; } }