1const DEFAULT_THEME_URL = "/themes/default.json?20260216.1"; 2const DARK_THEME_URL = "/themes/dark.json?20260217.1"; 3 4let default_theme = create_theme("Default", "default", {}); 5let dark_theme = create_theme("Dark", "dark", {}); 6let active_theme = null; 7let is_loading = false; 8let color_scheme_query = null; 9let applied_color_keys = new Set(); 10 11export async function initThemes() { 12 if (is_loading) { 13 return; 14 } 15 16 is_loading = true; 17 18 const [loaded_default, loaded_dark] = await Promise.all([ 19 load_theme(DEFAULT_THEME_URL, "Default", "default"), 20 load_theme(DARK_THEME_URL, "Dark", "dark") 21 ]); 22 23 default_theme = loaded_default; 24 dark_theme = merge_themes(default_theme, loaded_dark, "Dark", "dark"); 25 apply_preferred_theme(); 26 start_color_scheme_listener(); 27 is_loading = false; 28} 29 30function apply_preferred_theme() { 31 const next_theme = prefers_dark_mode() ? dark_theme : default_theme; 32 apply_theme(next_theme); 33} 34 35function start_color_scheme_listener() { 36 if (color_scheme_query || typeof window == "undefined" || typeof window.matchMedia != "function") { 37 return; 38 } 39 40 color_scheme_query = window.matchMedia("(prefers-color-scheme: dark)"); 41 const handle_change = () => { 42 apply_preferred_theme(); 43 }; 44 45 if (typeof color_scheme_query.addEventListener == "function") { 46 color_scheme_query.addEventListener("change", handle_change); 47 return; 48 } 49 50 if (typeof color_scheme_query.addListener == "function") { 51 color_scheme_query.addListener(handle_change); 52 } 53} 54 55function prefers_dark_mode() { 56 if (typeof window == "undefined" || typeof window.matchMedia != "function") { 57 return false; 58 } 59 return window.matchMedia("(prefers-color-scheme: dark)").matches; 60} 61 62function apply_theme(theme) { 63 if (!theme || typeof document == "undefined") { 64 return; 65 } 66 67 active_theme = theme; 68 const colors = theme.colors || {}; 69 const color_entries = Object.entries(colors).filter(([key, value]) => { 70 return Boolean(key) && typeof value == "string"; 71 }); 72 const next_color_keys = new Set(color_entries.map(([key]) => key)); 73 74 applied_color_keys.forEach((key) => { 75 if (!next_color_keys.has(key)) { 76 document.documentElement.style.removeProperty(key); 77 } 78 }); 79 80 color_entries.forEach(([key, value]) => { 81 document.documentElement.style.setProperty(key, value); 82 }); 83 84 applied_color_keys = next_color_keys; 85 86 document.documentElement.dataset.theme = theme.slug || "default"; 87 window.dispatchEvent(new CustomEvent("theme:applied", { detail: { theme: active_theme } })); 88} 89 90async function load_theme(url, fallback_name, fallback_slug) { 91 try { 92 const response = await fetch(url, { cache: "no-store" }); 93 if (!response.ok) { 94 throw new Error(`Failed to load theme from ${url}`); 95 } 96 const payload = await response.json(); 97 return normalize_theme(payload, fallback_name, fallback_slug); 98 } 99 catch (error) { 100 console.warn(`Unable to load theme from ${url}`, error); 101 return create_theme(fallback_name, fallback_slug, {}); 102 } 103} 104 105function normalize_theme(theme, fallback_name, fallback_slug) { 106 if (!theme || typeof theme != "object") { 107 return create_theme(fallback_name, fallback_slug, {}); 108 } 109 110 const raw_name = typeof theme.name == "string" ? theme.name.trim() : ""; 111 const name = raw_name || fallback_name; 112 const raw_slug = typeof theme.slug == "string" ? theme.slug.trim() : ""; 113 const computed_slug = raw_slug || slugify(name) || fallback_slug; 114 const colors = theme.colors && typeof theme.colors == "object" ? theme.colors : {}; 115 const cleaned_colors = {}; 116 117 Object.entries(colors).forEach(([key, value]) => { 118 if (typeof key != "string" || typeof value != "string") { 119 return; 120 } 121 122 const trimmed_key = key.trim(); 123 const trimmed_value = value.trim(); 124 if (!trimmed_key || !trimmed_value) { 125 return; 126 } 127 128 cleaned_colors[trimmed_key] = trimmed_value; 129 }); 130 131 return create_theme(name, computed_slug, cleaned_colors); 132} 133 134function merge_themes(base_theme, override_theme, fallback_name, fallback_slug) { 135 const base_colors = base_theme?.colors || {}; 136 const override_colors = override_theme?.colors || {}; 137 const name = override_theme?.name || fallback_name; 138 const slug = override_theme?.slug || fallback_slug; 139 140 return create_theme(name, slug, { 141 ...base_colors, 142 ...override_colors 143 }); 144} 145 146function create_theme(name, slug, colors) { 147 return { 148 name, 149 slug, 150 colors: colors || {} 151 }; 152} 153 154function slugify(value) { 155 return String(value || "") 156 .toLowerCase() 157 .replace(/[^a-z0-9]+/g, "-") 158 .replace(/^-+|-+$/g, "") 159 trim(); 160}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.