sized to 1em so existing
* font-size rules (.navbar .nav-link .bd-icon, .metadata .bd-icon, …) scale it.
*
* Theme-picker icons (sun/moon/snow/droplet/cup/half) are duplicated in
* blowfish.js so the toggle can swap them client-side; keep the two in sync.
*/
if (!function_exists('blowfish_icon')) {
function blowfish_icon($name) {
static $paths = array(
// --- UI ---
'calendar' => '',
'clock' => '',
'folder' => '',
'tag' => '',
'chevron-left' => '',
'chevron-right' => '',
'house' => '',
'link' => '',
'search' => '',
'share' => '',
// --- Theme picker (mirror in blowfish.js) ---
'sun' => '',
'moon' => '',
'snow' => '',
'droplet' => '',
'cup' => '',
'half' => '',
);
if (!isset($paths[$name])) { return ''; }
return '';
}
// Bludit already HTML-escapes titles/names on save (Sanitize::html), so a
// plain htmlspecialchars() in the theme double-encodes ("&" -> "&",
// shown as "&"). blowfish_text() decodes any existing entities first, then
// encodes exactly once — correct whether the value is pre-encoded or raw, and
// always output-safe. blowfish_plain() returns decoded text (for JSON-LD etc.).
function blowfish_plain($s) {
return html_entity_decode((string) $s, ENT_QUOTES, 'UTF-8');
}
function blowfish_text($s) {
return htmlspecialchars(blowfish_plain($s), ENT_QUOTES, 'UTF-8');
}
// Theme asset URL with an mtime cache-buster (so edited images/SVGs refetch).
function blowfish_asset($rel) {
$v = @filemtime(THEME_DIR . $rel);
return DOMAIN_THEME . $rel . ($v ? '?v=' . $v : '');
}
// Inject width/height attributes into local-upload
tags that lack
// them, so the browser reserves the box before the image loads (no CLS).
// Only touches images served from this site's uploads folder; remote
// images and tags that already declare a width are left alone.
function blowfish_img_dimensions($html) {
if (!defined('DOMAIN_UPLOADS') || !defined('PATH_UPLOADS')) { return $html; }
return preg_replace_callback('/
]*>/i', function ($m) {
$tag = $m[0];
if (preg_match('/\b(?:width|height)\s*=/i', $tag)) { return $tag; }
if (!preg_match('/\bsrc=["\']([^"\']+)["\']/i', $tag, $src)) { return $tag; }
$url = html_entity_decode($src[1], ENT_QUOTES, 'UTF-8');
if (strpos($url, DOMAIN_UPLOADS) !== 0) { return $tag; }
$rel = rawurldecode(substr($url, strlen(DOMAIN_UPLOADS)));
if ($rel === '' || strpos($rel, '..') !== false) { return $tag; }
$file = PATH_UPLOADS . $rel;
if (!is_file($file)) { return $tag; }
$size = @getimagesize($file);
if (!$size || empty($size[0]) || empty($size[1])) { return $tag; }
return preg_replace('/
'sun',
'dark' => 'moon',
'nord' => 'snow',
'dracula' => 'droplet',
'catppuccin' => 'cup',
);
return isset($map[$theme]) ? $map[$theme] : 'half';
}
}