$config['domain'], 'devMode' => $config['is_dev'], 'cookieHost' => $config['cookie_host'], ]); $body_class = []; if ($opts['full_width']) $body_class[] = 'full-width'; else if ($opts['wide']) $body_class[] = 'wide'; return << {$title} {$ctx->meta($meta)} {$ctx->renderStatic($static, $theme)} if_true($body_class, ' class="'.implode(' ', $body_class).'"')}> {$ctx->if_true($svg_defs, fn() => $ctx->renderSVGIcons($svg_defs))}
{$ctx->renderHeader($theme, $opts['head_section'], $opts['articles_lang'], $opts['is_index'])}
{$unsafe_body}
{$ctx->if_not($opts['full_width'], fn() => $ctx->renderFooter($admin_email))}
{$ctx->renderScript($js, $unsafe_lang)} {$ctx->if_not($opts['inside_admin_interface'], fn() => $ctx->render_external_counters())} {$ctx->if_admin(fn() => "")} HTML; } function render_external_counters($ctx) { return << HTML; } function renderSVGIcons($ctx, $svg_defs) { $buf = ''; foreach ($svg_defs as $name => $icon) { $buf .= << {$icon['svg']} SVG; } $buf .= ''; return $buf; } function renderScript($ctx, $unsafe_js, $unsafe_lang) { global $config; $styles = jsonEncode($ctx->styleNames); if ($config['is_dev']) $versions = '{}'; else { $versions = []; foreach ($config['static'] as $name => $v) { list($type, $bname) = getStaticNameParts($name); $versions[$type][$bname] = $v; } $versions = jsonEncode($versions); } return << StaticManager.init({$styles}, {$versions}); {$ctx->if_true($unsafe_js, '(function(){try{'.$unsafe_js.'}catch(e){window.console&&console.error("caught exception:",e)}})();')} {$ctx->if_true($unsafe_lang, 'extend(__lang, '.$unsafe_lang.');')} ThemeSwitcher.init(); HTML; } function meta($ctx, $meta) { if (empty($meta)) return ''; return implode('', array_map(function(array $item): string { $s = ' $v) $s .= ' '.htmlescape($k).'="'.htmlescape($v).'"'; $s .= '/>'; $s .= "\n"; return $s; }, $meta)); } function renderStatic($ctx, $static, $theme) { global $config; $html = []; $dark = $theme == 'dark'; $ctx->styleNames = []; foreach ($static as $name) { // javascript if (str_starts_with($name, 'js/')) $html[] = jsLink($name); // css else if (str_starts_with($name, 'css/')) { $html[] = cssLink($name, 'light', $style_name); $ctx->styleNames[] = $style_name; if ($dark) $html[] = cssLink($name, 'dark', $style_name); else if (!$config['is_dev']) $html[] = cssPrefetchLink($style_name.'_dark'); } else logError(__FUNCTION__.': unexpected static entry: '.$name); } return implode("\n", $html); } function jsLink(string $name): string { global $config; list (, $bname) = getStaticNameParts($name); if ($config['is_dev']) { $href = '/js.php?name='.urlencode($bname).'&v='.time(); } else { $href = '/dist-js/'.$bname.'.js?'.getStaticVersion($name); } return ''; } function cssLink(string $name, string $theme, &$bname = null): string { list(, $bname) = getStaticNameParts($name); $config_name = 'css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css'; if (is_dev()) { $href = '/sass.php?name='.urlencode($bname).'&theme='.$theme.'&v='.time(); } else { $version = getStaticVersion($config_name); $href = '/dist-css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css?'.$version; } $id = 'style_'.$bname; if ($theme == 'dark') $id .= '_dark'; return ''; } function cssPrefetchLink(string $name): string { $url = '/dist-css/'.$name.'.css?'.getStaticVersion('css/'.$name.'.css'); $integrity = getStaticIntegrityAttribute('css/'.$name.'.css'); return << HTML; } function getStaticNameParts(string $name): array { $dname = dirname($name); $bname = basename($name); if (($pos = strrpos($bname, '.'))) { $ext = substr($bname, $pos+1); $bname = substr($bname, 0, $pos); } else { $ext = ''; } return [$dname, $bname, $ext]; } function getStaticVersion(string $name): string { global $config; if ($config['is_dev']) return time(); if (str_starts_with($name, '/')) { logWarning(__FUNCTION__.': '.$name.' starts with /'); $name = substr($name, 1); } return $config['static'][$name]['version'] ?? 'notfound'; } function getStaticIntegrityAttribute(string $name): string { if (is_dev()) return ''; global $config; return ' integrity="'.implode(' ', array_map(fn($hash_type) => $hash_type.'-'.$config['static'][$name]['integrity'][$hash_type], RESOURCE_INTEGRITY_HASHES)).'"'; } function renderHeader(SkinContext $ctx, string $theme, ?string $section, ?string $articles_lang, bool $show_subtitle): string { $icons = svg(); $items = []; $items[] = ['url' => '/articles/'.($articles_lang ? '?lang='.$articles_lang : ''), 'label' => 'articles', 'selected' => $section === 'articles']; $items[] = ['url' => '/files/', 'label' => 'files', 'selected' => $section === 'files']; $items[] = ['url' => '/info/', 'label' => 'about', 'selected' => $section === 'about']; if (is_admin()) $items[] = ['url' => '/admin/', 'label' => $icons->settings_28(in_place: true), 'type' => 'settings', 'selected' => $section === 'admin']; $items[] = [ 'url' => 'javascript:void(0)', 'label' => $icons->moon_auto_18(in_place: true) .$icons->moon_light_18(in_place: true) .$icons->moon_dark_18(in_place: true), 'type' => 'theme-switcher', 'type_opts' => $theme ]; // here, items are rendered using for_each, so that there are no gaps (whitespaces) between tags $class = 'head'; if (!$show_subtitle) $class .= ' no-subtitle'; return <<
{$ctx->for_each($items, fn($item) => $ctx->renderHeaderItem( $item['url'], $item['label'], $item['type'] ?? false, $item['type_opts'] ?? null, $item['selected'] ?? false ))}
HTML; } function renderHeaderItem(SkinContext $ctx, string $url, ?Stringable $unsafe_label, ?string $type, ?string $type_opts, bool $selected): string { $args = ''; $class = ''; switch ($type) { case 'theme-switcher': $args = ' onclick="return ThemeSwitcher.next(event)"'; $class = ' is-theme-switcher '.$type_opts; break; case 'settings': $class = ' is-settings'; break; } if ($selected) $class .= ' is-selected'; return <<{$unsafe_label} HTML; } function renderFooter($ctx, $admin_email): string { return << Email: {$admin_email} HTML; }