All files / src/templates/sections header.ts

100% Statements 18/18
100% Branches 20/20
100% Functions 2/2
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                                    29x                                                                     455x 455x 455x 455x 455x 455x 455x 455x 455x 455x   455x       455x           455x   455x 1365x     455x       455x                                                              
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Templates/Sections/Header
 * @description Shared responsive site header used by every generated page family.
 * Split out of `section-builders.ts` (Refactor 8/8).
 */
 
import { escapeHTML } from '../../utils/file-utils.js';
import type { LanguageCode } from '../../types/index.js';
import {
  getLocalizedString,
  HEADER_SUBTITLE_LABELS,
  THEME_TOGGLE_LABELS,
  HEADER_CTA_SPONSOR_LABELS,
  HEADER_CTA_BECOME_SPONSOR_LABELS,
  HEADER_CTA_SECURITY_LABELS,
  FOOTER_POLITICAL_INTELLIGENCE_LABELS,
  LANGUAGE_SELECTION_ARIA_LABELS,
} from '../../constants/languages.js';
import { createThemeToggleButton } from '../../constants/config.js';
import { icon, type IconName } from '../icons.js';
import { buildResponsiveBannerPicture } from './banner.js';
 
/** Icon name used for security/transparency links across the chrome. */
const ICON_SECURITY: IconName = 'shield-star';
 
/**
 * Options for building the shared site header.
 */
export interface SiteHeaderOptions {
  /** Language code used for localization. */
  lang: LanguageCode;
  /**
   * URL path prefix prepended to relative asset and page links.
   * Use `''` for root pages and `'../'` for pages inside `news/`.
   */
  pathPrefix: string;
  /** Link target for the brand/logo. */
  homeHref: string;
  /** Accessible site title and visible brand title. */
  siteTitle: string;
  /** Pre-rendered language switcher links for the current page family. */
  languageSwitcherHtml: string;
  /**
   * Optional override for the Political Intelligence destination. When
   * omitted the link points at the language-aware
   * `political-intelligence[_<lang>].html` page using `pathPrefix`.
   * Pass an empty string to suppress the link altogether.
   */
  politicalIntelligenceHref?: string;
}
 
/**
 * Build the shared responsive site header used by every generated page family.
 *
 * @param options - {@link SiteHeaderOptions} controlling language, assets, and language links.
 * @returns HTML string for `<header class="site-header">…</header>`.
 */
export function buildSiteHeader(options: SiteHeaderOptions): string {
  const { lang, pathPrefix, homeHref, siteTitle, languageSwitcherHtml } = options;
  const headerSubtitle = escapeHTML(getLocalizedString(HEADER_SUBTITLE_LABELS, lang));
  const themeToggleLabel = getLocalizedString(THEME_TOGGLE_LABELS, lang);
  const sponsorLabel = escapeHTML(getLocalizedString(HEADER_CTA_SPONSOR_LABELS, lang));
  const becomeSponsorLabel = escapeHTML(getLocalizedString(HEADER_CTA_BECOME_SPONSOR_LABELS, lang));
  const securityLabel = escapeHTML(getLocalizedString(HEADER_CTA_SECURITY_LABELS, lang));
  const piLabel = escapeHTML(getLocalizedString(FOOTER_POLITICAL_INTELLIGENCE_LABELS, lang));
  const langSelectionLabel = escapeHTML(getLocalizedString(LANGUAGE_SELECTION_ARIA_LABELS, lang));
  const safeTitle = escapeHTML(siteTitle);
  const defaultPiHref = `${pathPrefix}${lang === 'en' ? 'political-intelligence.html' : `political-intelligence_${lang}.html`}`;
  const rawPiHref =
    typeof options.politicalIntelligenceHref === 'string'
      ? options.politicalIntelligenceHref
      : defaultPiHref;
  const isSafeHref =
    rawPiHref.length === 0 ||
    (rawPiHref.startsWith('/') && !rawPiHref.startsWith('//') && rawPiHref[1] !== '\\') ||
    rawPiHref.startsWith('./') ||
    rawPiHref.startsWith('../') ||
    rawPiHref.startsWith('https://') ||
    (!rawPiHref.includes(':') && !rawPiHref.startsWith('//') && !rawPiHref.startsWith('\\'));
  const piHref = isSafeHref ? rawPiHref : defaultPiHref;
 
  const cta = (extraClass: string, href: string, iconName: IconName, label: string): string =>
    `<a class="site-header__cta${extraClass ? ` ${extraClass}` : ''}" href="${href}" target="_blank" rel="noopener noreferrer" aria-label="${label}" title="${label}">${icon(iconName)}<span class="site-header__cta-label">${label}</span></a>`;
 
  const piCta =
    piHref.length > 0
      ? `<a class="site-header__cta site-header__cta--pi" href="${escapeHTML(piHref)}" aria-label="${piLabel}" title="${piLabel}">${icon('pi')}<span class="site-header__cta-label">${piLabel}</span></a>\n        `
      : '';
 
  return `<header class="site-header" role="banner">
    <div class="site-header__inner site-header__inner--stacked">
      <a href="${escapeHTML(homeHref)}" class="site-header__brand" aria-label="${safeTitle}">
        ${buildResponsiveBannerPicture({
          pathPrefix,
          pictureClass: 'site-header__logo-picture',
          imageClass: 'site-header__logo site-header__logo--banner',
          alt: siteTitle,
          sizes: '(max-width: 640px) 58vw, (max-width: 1200px) 15vw, 260px',
        })}
        <span class="site-header__brand-text">
          <span class="site-header__title">${safeTitle}</span>
          <span class="site-header__subtitle">${headerSubtitle}</span>
        </span>
      </a>
      <div class="site-header__actions">
        <div class="site-header__cta-group">
          ${piCta}${cta('site-header__cta--sponsor', 'https://github.com/sponsors/Hack23', 'heart', sponsorLabel)}
          ${cta('', 'https://www.hack23.com', 'sponsor', becomeSponsorLabel)}
          ${cta('site-header__cta--security', 'https://github.com/Hack23/euparliamentmonitor/blob/main/SECURITY.md', ICON_SECURITY, securityLabel)}
        </div>
        <div class="site-header__theme-toggle-slot">
          ${createThemeToggleButton(themeToggleLabel)}
        </div>
      </div>
      <nav class="site-header__langs" role="navigation" aria-label="${langSelectionLabel}">
        ${languageSwitcherHtml}
      </nav>
    </div>
  </header>`;
}