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 | 18x 18x 18x 4x 14x 20x 20x 18x 9x 7x 13x 5x 9x 9x 9x 9x 6x 6x 6x 3x 3x 9x 8x 12x 12x | // SPDX-License-Identifier: MIT
/**
* Theme persistence utilities for FOUC prevention.
*
* Provides testable implementations of the critical-path logic
* used in the blocking inline script (BaseLayout.astro lines 85-118)
* and the theme-switching UI script (lines 140-230).
*/
import { DEFAULT_THEME } from './constants.js';
import { migrateLegacyStorage, getSavedTheme } from './storage.js';
// Re-export for convenience
export { DEFAULT_THEME } from './constants.js';
/**
* Resolves the initial theme for FOUC prevention.
*
* Performs legacy storage migration, reads the stored theme, and validates
* it against the provided list of valid theme IDs. Returns the default
* theme if the stored value is missing or invalid.
*
* Mirrors the blocking inline script logic in BaseLayout.astro.
*/
export function resolveInitialTheme(windowObj: Window, validThemes: string[]): string {
migrateLegacyStorage(windowObj);
const saved = getSavedTheme(windowObj);
if (validThemes.indexOf(saved) === -1) {
return DEFAULT_THEME;
}
return saved;
}
/**
* Sanitizes a base URL to prevent XSS via protocol injection.
* Strips leading whitespace and control characters, then rejects
* protocol-relative and absolute URLs. Only allows relative paths.
*/
export function sanitizeBaseUrl(raw: string): string {
const normalized = raw.replace(/^[\s\u0000-\u001F\u007F]+/, '');
if (normalized.startsWith('//')) return '';
if (/^[a-z][a-z0-9+.-]*:/i.test(normalized)) return '';
return normalized;
}
/**
* Builds the full CSS href path for a theme.
*/
export function buildThemeCssHref(baseUrl: string, themeId: string): string {
return `${baseUrl}/assets/css/themes/turbo/${themeId}.css`;
}
/**
* Determines if the theme CSS link element needs updating.
*
* The default theme CSS is pre-loaded in the HTML, so we skip the
* update to avoid an unnecessary network request and style flash.
*/
export function needsCssUpdate(
themeId: string,
defaultTheme: string = DEFAULT_THEME,
): boolean {
return themeId !== defaultTheme;
}
/**
* Builds the theme icon image src path.
*/
export function buildThemeIconSrc(
baseUrl: string,
themeIcons: Record<string, string>,
themeId: string,
fallbackIcon: string = 'catppuccin-logo-macchiato.png',
): string {
return `${baseUrl}/assets/img/${themeIcons[themeId] || fallbackIcon}`;
}
/**
* Applies the initial theme to prevent FOUC (Flash of Unstyled Content).
*
* This is the testable equivalent of the blocking inline script in
* BaseLayout.astro. It:
* 1. Migrates legacy storage keys
* 2. Reads and validates the stored theme
* 3. Sets the `data-theme` attribute on `<html>`
* 4. Sets `window.__INITIAL_THEME__` for downstream scripts
* 5. Updates the CSS link href if the theme differs from the default
*/
export function applyInitialTheme(
doc: Document,
windowObj: Window,
validThemes: string[],
): string {
const theme = resolveInitialTheme(windowObj, validThemes);
doc.documentElement.setAttribute('data-theme', theme);
windowObj.__INITIAL_THEME__ = theme;
if (needsCssUpdate(theme)) {
const baseUrl = sanitizeBaseUrl(doc.documentElement.getAttribute('data-baseurl') || '');
const themeLink = doc.getElementById('turbo-theme-css') as HTMLLinkElement | null;
if (themeLink) {
const href = buildThemeCssHref(baseUrl, theme);
themeLink.href = new URL(href, windowObj.location.href).pathname;
}
}
return theme;
}
/**
* Updates active theme state on theme option elements.
*
* Toggles the `active` class on each element based on whether its
* `data-theme-id` or `data-theme` attribute matches the given active theme ID.
*/
export function updateActiveTheme(options: ArrayLike<Element>, activeTheme: string): void {
Array.from(options).forEach((opt) => {
const themeId = opt.getAttribute('data-theme-id') || opt.getAttribute('data-theme');
opt.classList.toggle('active', themeId === activeTheme);
});
}
|