feat(icons): add the FA5 catalog, REST endpoint and unified icon helpers
Mirror the reference theme's universal picker on FA5: a generated full catalog of the bundled FA5 Free 5.15.4 fonts (src/Icons/fa5-icons.php, generated from the shipped CSS + SVG fonts), a zibsvg group parsed from the runtime sprite so it cannot drift, and GET /aksrt/v1/icons/catalog. Shared admin pieces: aksrt_icon_markup()/aksrt_sanitize_icon_value() with an SVG allowlist, the picker field/modal assets, and one canonical aksrt-font-awesome handle registered through aksrt_enqueue_fontawesome().
This commit is contained in:
1 parent
9496db5171
commit
f629a4f373
12 files changed
+2541
-18
No files matched your search
@@ -239,6 +239,146 @@ function aksrt_contact_icon_options($selected = '') {
|
||||
}
|
||||
endif;
|
||||
|
||||
// ============================================================
|
||||
// 统一图标渲染(FA5 / 内置 SVG / 自定义 SVG)
|
||||
// ============================================================
|
||||
if (!function_exists('aksrt_enqueue_fontawesome')) :
|
||||
/**
|
||||
* Register (once) and enqueue the bundled Font Awesome 5 pack under the
|
||||
* single canonical handle. Every context — public, admin screens, widgets,
|
||||
* the icon picker — must go through this helper.
|
||||
*
|
||||
* @param array $deps Style dependencies for the first caller.
|
||||
* @return string The canonical handle.
|
||||
*/
|
||||
function aksrt_enqueue_fontawesome(array $deps = []): string {
|
||||
if (!wp_style_is('aksrt-font-awesome', 'registered')) {
|
||||
wp_register_style('aksrt-font-awesome', AKSRT_URI . '/assets/fontawesome5/css/all.min.css', $deps, '5.15.4');
|
||||
}
|
||||
wp_enqueue_style('aksrt-font-awesome');
|
||||
return 'aksrt-font-awesome';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_svg_kses_tags')) :
|
||||
/**
|
||||
* Allowed SVG tags/attributes for user-supplied custom SVG icon markup.
|
||||
*
|
||||
* @return array<string, array<string, bool>>
|
||||
*/
|
||||
function aksrt_svg_kses_tags(): array {
|
||||
static $tags = null;
|
||||
if ($tags !== null) {
|
||||
return $tags;
|
||||
}
|
||||
|
||||
$tags = [
|
||||
'svg' => ['class' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'fill' => true, 'xmlns' => true, 'aria-hidden' => true],
|
||||
'use' => ['href' => true, 'xlink:href' => true],
|
||||
'path' => ['d' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true],
|
||||
'g' => ['fill' => true, 'transform' => true],
|
||||
];
|
||||
return $tags;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_icon_markup')) :
|
||||
/**
|
||||
* Render an icon-picker value of any supported format.
|
||||
*
|
||||
* Supported values:
|
||||
* - "fa fas fa-star" / "fa fab fa-weixin" — FA5 class string
|
||||
* - "fa-star" — bare glyph, rendered as solid FA5
|
||||
* - "zibsvg-hot" — theme built-in SVG sprite key
|
||||
* - "customsvg-<svg …>" — inline user SVG markup
|
||||
*
|
||||
* @param string $value Raw stored value.
|
||||
* @param array|string $extra_classes Extra classes for the rendered element.
|
||||
* @return string Safe HTML, empty string for empty values.
|
||||
*/
|
||||
function aksrt_icon_markup(string $value, $extra_classes = []): string {
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$extra = '';
|
||||
if (is_string($extra_classes) && $extra_classes !== '') {
|
||||
$extra_classes = preg_split('/\s+/', $extra_classes) ?: [];
|
||||
}
|
||||
if (is_array($extra_classes)) {
|
||||
$clean = array_filter(array_map('sanitize_html_class', $extra_classes));
|
||||
if ($clean !== []) {
|
||||
$extra = ' ' . implode(' ', $clean);
|
||||
}
|
||||
}
|
||||
|
||||
if (str_starts_with($value, 'customsvg-')) {
|
||||
$svg = wp_kses(substr($value, 10), aksrt_svg_kses_tags());
|
||||
if ($svg === '') {
|
||||
return '';
|
||||
}
|
||||
if ($extra !== '' && stripos($svg, '<svg') === 0 && stripos($svg, 'class=') === false) {
|
||||
$svg = preg_replace('/<svg\b/i', '<svg class="' . ltrim($extra) . '"', $svg, 1);
|
||||
}
|
||||
return $svg;
|
||||
}
|
||||
|
||||
if (str_starts_with($value, 'zibsvg-')) {
|
||||
$key = sanitize_key(substr($value, 7));
|
||||
if ($key === '') {
|
||||
return '';
|
||||
}
|
||||
$class = $extra !== '' ? ltrim($extra) : 'icon';
|
||||
return '<svg class="' . esc_attr($class) . '" aria-hidden="true"><use xlink:href="#icon-' . $key . '"></use></svg>';
|
||||
}
|
||||
|
||||
if (!str_contains($value, 'fa ') && str_contains($value, 'fa-')) {
|
||||
$value = 'fa ' . $value;
|
||||
}
|
||||
return '<i class="' . esc_attr($value . $extra) . '" aria-hidden="true"></i>';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_sanitize_icon_value')) :
|
||||
/**
|
||||
* Sanitize an icon-picker value. Keeps the SVG markup of customsvg- values
|
||||
* (plain sanitize_text_field would strip the tags), filters FA/zip keys.
|
||||
*
|
||||
* @param string $value Raw value from a form/REST payload.
|
||||
* @return string Sanitized value.
|
||||
*/
|
||||
function aksrt_sanitize_icon_value(string $value): string {
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (str_starts_with($value, 'customsvg-')) {
|
||||
$svg = wp_kses(substr($value, 10), aksrt_svg_kses_tags());
|
||||
return $svg === '' ? '' : 'customsvg-' . $svg;
|
||||
}
|
||||
|
||||
return sanitize_text_field($value);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_empty_state_svgs')) :
|
||||
/**
|
||||
* Shared empty-state SVG icons (24px stroke style, not sprite keys).
|
||||
*
|
||||
* @return array{post:string,comment:string,favorite:string,follow:string}
|
||||
*/
|
||||
function aksrt_empty_state_svgs(): array {
|
||||
return [
|
||||
'post' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h6M9 9h1"/></svg>',
|
||||
'comment' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
||||
'favorite' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
|
||||
'follow' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
|
||||
];
|
||||
}
|
||||
endif;
|
||||
|
||||
// ============================================================
|
||||
// Public social links (author hero / profile)
|
||||
// ============================================================
|
||||
|
||||
Reference in new issue
Block a user