Changeset 495b019 in Klonkt for src/services/ThemeService.js
- Timestamp:
- 10/25/2026 09:52:01 AM (2 days ago)
- Children:
- b4aaadd
- Parents:
- 5d0d3f35
- git-author:
- Bart <bart@…> (10/14/2026 02:48:46 PM)
- git-committer:
- Bart <bart@…> (10/25/2026 09:52:01 AM)
- File:
-
- 1 edited
-
src/services/ThemeService.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ThemeService.js
r5d0d3f35 r495b019 1 import fs from 'fs'; 2 import path from 'path'; 3 import { fileURLToPath } from 'url'; 4 1 5 /** 2 6 * ThemeService — Palette and theme management … … 207 211 }; 208 212 } 213 214 // ── Lay-outthema's (experimental/theme, op weg naar 2.0) ───────────── 215 // 216 // Een lay-out is een PAKKET: een map met een theme.json en een stylesheet. 217 // De ingebouwde staan in src/themes/<id>/; een thema van buiten installeren 218 // komt later en gaat door dezelfde lader, zodat er één vorm is. 219 // 220 // Wie kiest: de BEHEERDER van een site (sites.layout). Een bezoeker niet -- 221 // `?layout=` is alleen een voorproefje voor wie de site beheert. 222 223 /** Het DOM-contract dat thema's mogen aanspreken. Gaat omhoog zodra de 224 * markup verandert op een manier die thema's breekt; een thema op een ander 225 * contract valt dan terug op de standaardlay-out in plaats van half te 226 * werken. */ 227 static LAYOUT_CONTRACT = 1; 228 229 static THEMES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'themes'); 230 231 /** Een id is een naam en geen pad. Kort, kleine letters, punten en 232 * streepjes, zodat er niets anders van te maken is dan een mapnaam. */ 233 static isLayoutId(id) { 234 return typeof id === 'string' && /^[a-z0-9][a-z0-9.-]{0,63}$/.test(id) && !id.includes('..'); 235 } 236 237 /** Een theme.json die klopt, of null -- met de reden in de log, want een 238 * thema dat stil wegvalt is een raadsel voor wie het installeerde. */ 239 static validateLayoutManifest(m, dirName) { 240 const fout = (waarom) => { console.warn(`[theme] ${dirName || '?'}: ${waarom}`); return null; }; 241 if (!m || typeof m !== 'object') return fout('geen geldige theme.json'); 242 if (!ThemeService.isLayoutId(m.id)) return fout(`ongeldig id ${JSON.stringify(m.id)}`); 243 if (dirName && m.id !== dirName) return fout(`id ${m.id} hoort in een map met die naam`); 244 if (typeof m.name !== 'string' || !m.name.trim()) return fout('naam ontbreekt'); 245 if (typeof m.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(m.version)) return fout('versie is geen x.y.z'); 246 if (m.contract !== ThemeService.LAYOUT_CONTRACT) return fout(`contract ${m.contract}, deze Klonkt kent ${ThemeService.LAYOUT_CONTRACT}`); 247 if (typeof m.stylesheet !== 'string' || !/^[a-z0-9][a-z0-9._-]*\.css$/i.test(m.stylesheet)) return fout('stylesheet ontbreekt of is geen bestandsnaam'); 248 return { 249 id: m.id, 250 name: m.name.trim().slice(0, 80), 251 description: typeof m.description === 'string' ? m.description.slice(0, 500) : '', 252 version: m.version, 253 contract: m.contract, 254 stylesheet: m.stylesheet, 255 author: typeof m.author === 'string' ? m.author.slice(0, 200) : '', 256 license: typeof m.license === 'string' ? m.license.slice(0, 64) : '', 257 builtIn: true, 258 }; 259 } 260 261 static _layouts = null; 262 263 /** Alle lay-outs die deze Klonkt kent, op naam. Eén keer gelezen per proces: 264 * een pakket verandert niet terwijl de server draait, en de shell vraagt 265 * hier bij elk verzoek om. */ 266 static listLayouts() { 267 if (ThemeService._layouts) return ThemeService._layouts; 268 const uit = []; 269 let mappen = []; 270 try { mappen = fs.readdirSync(ThemeService.THEMES_DIR, { withFileTypes: true }); } catch { mappen = []; } 271 for (const d of mappen) { 272 if (!d.isDirectory() || !ThemeService.isLayoutId(d.name)) continue; 273 const dir = path.join(ThemeService.THEMES_DIR, d.name); 274 let ruw; 275 try { ruw = JSON.parse(fs.readFileSync(path.join(dir, 'theme.json'), 'utf8')); } catch { console.warn(`[theme] ${d.name}: theme.json onleesbaar`); continue; } 276 const m = ThemeService.validateLayoutManifest(ruw, d.name); 277 if (!m) continue; 278 if (!fs.existsSync(path.join(dir, m.stylesheet))) { console.warn(`[theme] ${d.name}: ${m.stylesheet} ontbreekt`); continue; } 279 uit.push(m); 280 } 281 uit.sort((a, b) => a.name.localeCompare(b.name)); 282 ThemeService._layouts = uit; 283 return uit; 284 } 285 286 static getLayout(id) { 287 if (!ThemeService.isLayoutId(id)) return null; 288 return ThemeService.listLayouts().find((m) => m.id === id) || null; 289 } 290 291 /** Waar een thema zijn bestanden laat zien. De versie zit in het pad, dus 292 * een nieuwe versie is een nieuwe URL en de cache van een jaar kan blijven. */ 293 static layoutBaseUrl(m) { return `/themes/${m.id}/${m.version}`; } 294 static layoutStylesheetUrl(m) { return `${ThemeService.layoutBaseUrl(m)}/${m.stylesheet}`; } 295 static layoutManifestUrl(m) { return `${ThemeService.layoutBaseUrl(m)}/theme.json`; } 296 297 /** 298 * Welke lay-out deze pagina draagt, of null voor de gewone kop. 299 * 300 * - Beheerpagina's ALTIJD de gewone kop: een kapot thema mag de beheerder 301 * nooit buitensluiten van de instelling waarmee hij het uitzet. 302 * - `preview` (uit ?layout=) alleen voor wie de site beheert. Een bezoeker 303 * kiest niet; de site kiest. 304 * - Anders wat de site gekozen heeft. 305 */ 306 static resolveLayout(site, { isAdminPage = false, canManage = false, preview = null } = {}) { 307 if (isAdminPage) return null; 308 if (canManage && typeof preview === 'string') { 309 if (preview === 'top') return null; 310 const p = ThemeService.getLayout(preview); 311 if (p) return p; 312 } 313 return site && site.layout ? ThemeService.getLayout(site.layout) : null; 314 } 315 316 /** Het bestand achter /themes/<id>/<versie>/<naam>, of null. Alleen wat het 317 * pakket zelf noemt -- de stylesheet en de theme.json -- en alleen van de 318 * versie die er nu is. Geen pad uit de URL komt ooit in een join terecht. */ 319 static layoutFile(id, version, file) { 320 const m = ThemeService.getLayout(id); 321 if (!m || version !== m.version) return null; 322 if (file !== m.stylesheet && file !== 'theme.json') return null; 323 return path.join(ThemeService.THEMES_DIR, m.id, file); 324 } 325 326 /** Wat de actor erover zegt (shaer:theme), zodat Shaer het thema van zijn 327 * eigen site kan ophalen in plaats van er zelf een te kiezen. */ 328 static layoutActorProp(base, site) { 329 const m = site && site.layout ? ThemeService.getLayout(site.layout) : null; 330 if (!m) return {}; 331 return { 332 'shaer:theme': { 333 type: 'shaer:Theme', 334 id: `${base}${ThemeService.layoutManifestUrl(m)}`, 335 name: m.name, 336 'shaer:version': m.version, 337 'shaer:contract': m.contract, 338 }, 339 }; 340 } 209 341 } 210 342
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)