Files
markdown-converter/src/main/ThemeRegistry.js
T
Amit Haridas 1e7e332c24 feat(theme-registry): pure ThemeRegistry module with full API
- 8 exports: register/unregister/list/get/categories/lightThemes/darkThemes/clear
- kebab-case id validator; category whitelist (light/dark/high-contrast/seasonal)
- duplicate-id and shape errors with descriptive messages
- pure CommonJS, no IO, no Electron deps — T2 bootstrap will register all 37 themes
- 10 Jest tests covering register/unregister/get/categories/light+dark filters
- full suite: 77 suites, 924 tests passing; lint + Prettier clean

Amit Haridas
2026-09-14 19:39:52 +05:30

109 lines
2.9 KiB
JavaScript

/**
* Editor Theme Registry — single source of truth for installed editor themes.
*
* Pure module: no Electron, no IO, no globals beyond a module-scoped Map.
* The bootstrap (`ThemeRegistry.bootstrap.js`) calls `register()` at startup;
* `main.js` menu builder calls `list()` + `categories()`; `setTheme()`
* validation calls `get()`.
*
* Theme shape:
* { id, label, category: 'light'|'dark'|'high-contrast'|'seasonal', isDark }
*
* Ids are kebab-case lowercase. Categories are independent of `isDark` —
* e.g. `winter-is-coming-light` has `isDark: false` and `category: 'light'`,
* but a future seasonal theme might pair `category: 'seasonal'` with
* `isDark: true`. Filtering by category is the public contract; `isDark`
* is metadata for the renderer.
*
* @module ThemeRegistry
*/
const VALID_CATEGORIES = new Set(['light', 'dark', 'high-contrast', 'seasonal']);
const ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
const themes = new Map(); // id → theme
function validate(theme) {
if (!theme || typeof theme !== 'object') throw new Error('theme must be an object');
if (typeof theme.id !== 'string' || !theme.id) throw new Error('theme is missing id');
if (!ID_RE.test(theme.id)) {
throw new Error(`theme id must be kebab-case lowercase: got "${theme.id}"`);
}
if (typeof theme.label !== 'string' || !theme.label) {
throw new Error(`theme "${theme.id}" is missing label`);
}
if (!('category' in theme) || theme.category === undefined || theme.category === null) {
throw new Error(`theme "${theme.id}" is missing category`);
}
if (!VALID_CATEGORIES.has(theme.category)) {
throw new Error(
`theme "${theme.id}" has invalid category "${theme.category}" (allowed: ${[...VALID_CATEGORIES].join(', ')})`
);
}
if (typeof theme.isDark !== 'boolean') {
throw new Error(`theme "${theme.id}" is missing isDark boolean`);
}
}
function register(theme) {
validate(theme);
if (themes.has(theme.id)) {
throw new Error(`duplicate theme id: ${theme.id}`);
}
themes.set(theme.id, {
id: theme.id,
label: theme.label,
category: theme.category,
isDark: theme.isDark,
});
return theme;
}
function unregister(id) {
return themes.delete(id);
}
function list() {
return [...themes.values()];
}
function get(id) {
return themes.get(id) || null;
}
function categories() {
const seen = new Set();
const out = [];
for (const t of themes.values()) {
if (!seen.has(t.category)) {
seen.add(t.category);
out.push(t.category);
}
}
return out;
}
function lightThemes() {
return list().filter((t) => t.category === 'light');
}
function darkThemes() {
return list().filter((t) => t.category === 'dark');
}
/** Test-only: empty the registry between tests. */
function clear() {
themes.clear();
}
module.exports = {
register,
unregister,
list,
get,
categories,
lightThemes,
darkThemes,
clear,
};