Files
wordpress-theme-aksrt/inc/helpers/aksrt-icons.php
T
kate522 c472045ce4 fix(auth): align the social-login surfaces with the reference theme
The "other login methods" block drifted from zibll on three axes. Gitee was
mapped to fa-code-branch because FA5 Free ships no Gitee brand glyph, so the
button showed a generic branch icon; Google showed the monochrome FA glyph
instead of the reference's multi-colour mark. The modal also printed an
implicit agreement line under the social buttons, which the reference guest
card does not have. And the top-nav user popover built its own
.social-login-divider / .oauth-row / .oauth-btn markup and then wrapped that
in a second separator and .social_loginbar, so the block rendered nested and
unlike the reference.

aksrt_brand_icon() now routes zibsvg-/customsvg- values through the shared
icon renderer, and the brand map resolves gitee to zibsvg-gitee and google to
zibsvg-google-color. Both sprite keys already exist in the runtime symbol
sheet, which the loader pulls in on every frontend page. Because
get_auth_providers_data() and binding_row() both go through
aksrt_brand_icon(), one map change fixes the icon on the modal, the user card,
the top-nav popover, the user-center binding list and the profile social
links.

render_social_buttons() is rewritten to the reference shape: an optional
.social-separator, a .social_loginbar, and one .social-login-item <type>
toggle-radius anchor per enabled provider with rel="nofollow" and a
"<label>登录" title. oauth_button_lg switches to the button-lg variant with a
label, and the WeChat entry carries qrcode-signin so auth.js opens the QR
panel instead of navigating, matching the reference. The navbar and the link
apply modal now append the block verbatim instead of re-wrapping it, and the
user-card loop adopts the same item attributes. The retired
.social-login-divider / .oauth-row / .oauth-btn rules are dropped from
base-1.css, and the binding-list icon rule gains an svg selector so the
sprite icons keep the 22px the FA glyphs had.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and visual (613 tests), plus php -l on every
changed PHP file. Extended the auth source contract with the agreement-line,
brand-icon and popover-structure cases, and pinned the user-card guest
markup.
2026-10-17 14:42:42 +08:00

472 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* AKSRT Theme — Brand / contact icons (Font Awesome 5)
*
* Short-key → FA5 class map, escaped <i> markup, contact option labels,
* and public social-link renderer. Local SVG inventory removed.
*
* @package AKSRT
* @version 3.4.0
*/
defined('ABSPATH') || exit;
// ============================================================
// PHP 7.x 兼容辅助函数(替代 PHP 8.0+ 的 match 表达式)
// ============================================================
if (!function_exists('aksrt_parse_country_list')):
/**
* 解析 GeoIP 屏蔽国家列表字符串为数组
*
* @param string $raw 逗号分隔的国家代码
* @return array 大写国家代码数组
*/
function aksrt_parse_country_list($raw) {
if (empty($raw)) return array();
$parts = explode(',', wp_unslash($raw));
$result = array();
foreach ($parts as $p) {
$trimmed = trim($p);
if ($trimmed !== '') {
$result[] = strtoupper($trimmed);
}
}
return array_values(array_filter($result));
}
endif;
// ============================================================
// Brand icon map (FA 5.15.4)
// ============================================================
if (!function_exists('aksrt_brand_icon_map')) :
/**
* Short key → FA5 class string or built-in SVG sprite key (allowlisted).
*
* @return array<string, string>
*/
function aksrt_brand_icon_map(): array {
static $map = null;
if ($map !== null) {
return $map;
}
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so
// those two resolve to the theme's built-in SVG sprite (zibsvg-*) instead of
// a solid stand-in; every other key stays on FA5.
$map = [
'github' => 'fa fab fa-github',
'google' => 'zibsvg-google-color',
'x' => 'fa fab fa-twitter',
'twitter' => 'fa fab fa-twitter',
'email' => 'fa fas fa-envelope',
'envelope' => 'fa fas fa-envelope',
'wechat' => 'fa fab fa-weixin',
'weixin' => 'fa fab fa-weixin',
'qq' => 'fa fab fa-qq',
'weibo' => 'fa fab fa-weibo',
'telegram' => 'fa fab fa-telegram-plane',
'discord' => 'fa fab fa-discord',
'bilibili' => 'fa fas fa-tv',
'linkedin' => 'fa fab fa-linkedin',
'facebook' => 'fa fab fa-facebook-f',
'youtube' => 'fa fab fa-youtube',
'instagram' => 'fa fab fa-instagram',
'web' => 'fa fas fa-globe',
'globe' => 'fa fas fa-globe',
'gitee' => 'zibsvg-gitee',
'baidu' => 'fa fas fa-paw',
'alipay' => 'fa fab fa-alipay',
];
return $map;
}
endif;
if (!function_exists('aksrt_normalize_icon_key')) :
/**
* Normalize short key or full FA class string to a short map key.
*
* Examples: 'fa fab fa-github' | 'fa-github' | 'github' | 'weixin' → 'github' | 'wechat'
*
* Legacy FA4 values (a bare `fa` token followed by `fa-<glyph>`) are stripped too.
*
* @param string $name Raw icon key or FA class string.
* @return string Short key (may be empty).
*/
function aksrt_normalize_icon_key(string $name): string {
$name = trim($name);
if ($name === '') {
return '';
}
// Strip the FA5 layout/style prefixes, the glyph prefix and the legacy FA4
// double-`fa` form (a bare `fa` token followed by `fa-<glyph>`). The site
// ships FA5 only, but stored values may predate it.
$name = preg_replace('/^fa\s+(?:fas|far|fab)\s+/i', '', $name);
$name = preg_replace('/^fa\s+fa-/i', '', (string) $name);
$name = preg_replace('/^(?:fas|far|fab)\s+/i', '', (string) $name);
$name = preg_replace('/^fa-/i', '', (string) $name);
$name = trim((string) $name, "- \t\n\r\0\x0B");
$name = strtolower($name);
// Aliases → canonical keys used by the map / contact options.
$aliases = [
'weixin' => 'wechat',
'weixingzh' => 'wechat',
'twitter' => 'x',
'envelope' => 'email',
'globe' => 'web',
];
if (isset($aliases[$name])) {
return $aliases[$name];
}
return $name;
}
endif;
if (!function_exists('aksrt_brand_icon_class')) :
/**
* Resolve a short key, FA class string or sprite key to its icon value.
*
* @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
* @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
*/
function aksrt_brand_icon_class(string $name): string {
$raw = trim($name);
if ($raw === '') {
return '';
}
// Built-in SVG sprite and inline SVG values pass straight through; they are
// not FA class strings and must survive to the markup renderer untouched.
if (preg_match('/^(?:zibsvg|customsvg)-/i', $raw)) {
return $raw;
}
// Pass through already-valid full FA class strings (footer contact_more).
if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) {
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
$key = aksrt_normalize_icon_key($raw);
$map = aksrt_brand_icon_map();
if ($key !== '' && isset($map[$key])) {
return $map[$key];
}
// Allowlisted style + glyph only — collapse whitespace, lower style prefix.
$parts = preg_split('/\s+/', strtolower($raw));
$parts = array_values(array_filter((array) $parts));
return implode(' ', $parts);
}
$key = aksrt_normalize_icon_key($raw);
if ($key === '') {
return '';
}
$map = aksrt_brand_icon_map();
return $map[$key] ?? '';
}
endif;
if (!function_exists('aksrt_brand_icon')) :
/**
* Escaped markup for a brand/contact icon (FA5 <i> or built-in SVG sprite).
*
* @param string $name Short key, FA class string or zibsvg-/customsvg- value.
* @param array|string $extra_classes Optional extra classes.
* @return string Safe HTML or empty string when unknown.
*/
function aksrt_brand_icon(string $name, $extra_classes = []): string {
$class = aksrt_brand_icon_class($name);
// Sprite and inline-SVG values carry their own element, so the shared icon
// renderer owns the markup (it emits <svg class="icon"><use …>).
if (strpos($class, 'zibsvg-') === 0 || strpos($class, 'customsvg-') === 0) {
return aksrt_icon_markup($class, $extra_classes);
}
if ($class === '') {
return '';
}
if (is_string($extra_classes) && $extra_classes !== '') {
$extra_classes = preg_split('/\s+/', $extra_classes) ?: [];
}
if (is_array($extra_classes) && $extra_classes !== []) {
$extra = array_filter(array_map('sanitize_html_class', $extra_classes));
if ($extra !== []) {
$class .= ' ' . implode(' ', $extra);
}
}
return '<i class="' . esc_attr($class) . '" aria-hidden="true"></i>';
}
endif;
// ============================================================
// 图标 key → 显示名称
// ============================================================
if (!function_exists('aksrt_icon_key_to_name')) :
function aksrt_icon_key_to_name($key) {
$key = aksrt_normalize_icon_key((string) $key);
$map = array(
'github' => 'GitHub', 'google' => 'Google', 'x' => 'X',
'email' => '邮箱', 'wechat' => '微信', 'qq' => 'QQ',
'weibo' => '微博', 'telegram' => 'Telegram',
'discord' => 'Discord', 'bilibili' => 'Bilibili',
'linkedin' => 'LinkedIn', 'facebook' => 'Facebook',
'youtube' => 'YouTube', 'instagram' => 'Instagram',
'web' => '网站',
'gitee' => 'Gitee',
'baidu' => 'Baidu',
'alipay' => 'Alipay',
);
return isset($map[$key]) ? $map[$key] : ucwords(str_replace('-', ' ', $key));
}
endif;
// ============================================================
// 联系方式图标选项(仅保留选定的 15 个)
// ============================================================
if (!function_exists('aksrt_contact_icon_options')) :
function aksrt_contact_icon_options($selected = '') {
$icons = array(
'github' => 'GitHub',
'google' => 'Google',
'x' => 'X',
'email' => '邮箱',
'wechat' => '微信',
'qq' => 'QQ',
'weibo' => '微博',
'telegram' => 'Telegram',
'discord' => 'Discord',
'bilibili' => 'Bilibili',
'linkedin' => 'LinkedIn',
'facebook' => 'Facebook',
'youtube' => 'YouTube',
'instagram' => 'Instagram',
'web' => '网站',
);
$selected = aksrt_normalize_icon_key((string) $selected);
$html = '<option value="">—</option>';
foreach ($icons as $key => $label) {
$sel = $key === $selected ? ' selected' : '';
$html .= '<option value="' . esc_attr($key) . '"' . $sel . '>' . esc_html($label) . '</option>';
}
return $html;
}
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;
}
// Shape list mirrors the admin picker's cleanSvg() accept test — every
// element it lets through must survive the kses pass, otherwise a pasted
// <circle>/<rect>/… SVG is silently saved without its shapes.
$shape_attrs = ['fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true];
$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] + $shape_attrs,
'g' => ['fill' => true, 'transform' => true],
'circle' => ['cx' => true, 'cy' => true, 'r' => true] + $shape_attrs,
'ellipse' => ['cx' => true, 'cy' => true, 'rx' => true, 'ry' => true] + $shape_attrs,
'rect' => ['x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true] + $shape_attrs,
'polygon' => ['points' => true] + $shape_attrs,
'line' => ['x1' => true, 'y1' => true, 'x2' => true, 'y2' => true] + $shape_attrs,
];
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
* - legacy FA4 string — double-`fa` form, upgraded to FA5
* - "zibsvg-hot" — theme built-in SVG sprite key
* - "customsvg-<svg …>" — inline user SVG markup
*
* Output is always FA5 — the legacy FA4 double-`fa` prefix is never emitted.
*
* @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>';
}
// 本站统一 FA5 输出:遗留 FA4 双 fa 前缀、裸字形 "fa-xxx" 与缺少 fa 前缀的
// "fas/far/fab fa-xxx" 均归一为 "fa fas fa-xxx"(末尾附加类名原样保留)。
if (preg_match('/^fa\s+fa-/i', $value)) {
$value = preg_replace('/^fa\s+/i', 'fa fas ', $value, 1);
} elseif (preg_match('/^fa-/i', $value)) {
$value = 'fa fas ' . $value;
} elseif (preg_match('/^(?:fas|far|fab)\s+fa-/i', $value)) {
$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)
// ============================================================
if (!function_exists('aksrt_render_social_links')) :
/**
* Echo a list of public contact links with FA icons.
*
* @param array<int, array{name?:string,url?:string,icon?:string,title?:string,label?:string}> $contacts
*/
function aksrt_render_social_links(array $contacts): void {
$links = [];
foreach ($contacts as $contact) {
if (!is_array($contact)) {
continue;
}
$url = isset($contact['url']) ? (string) $contact['url'] : '';
$icon = isset($contact['icon']) ? (string) $contact['icon'] : '';
if ($url === '' || $icon === '') {
continue;
}
$icon_html = aksrt_brand_icon($icon);
if ($icon_html === '') {
$icon_html = '<i class="fa fas fa-globe" aria-hidden="true"></i>';
}
$label = (string) ($contact['name'] ?? $contact['title'] ?? $contact['label'] ?? '');
if ($label === '') {
$label = aksrt_icon_key_to_name($icon);
}
$links[] = [
'url' => $url,
'label' => $label,
'icon' => $icon_html,
];
}
if ($links === []) {
return;
}
echo '<div class="profile-social-links">';
foreach ($links as $link) {
printf(
'<a class="social-icon-link" href="%1$s" target="_blank" rel="noopener noreferrer" title="%2$s" aria-label="%2$s">%3$s</a>',
esc_url($link['url']),
esc_attr($link['label']),
$link['icon'] // pre-escaped via aksrt_brand_icon / fixed globe fallback
);
}
echo '</div>';
}
endif;