Changeset 495b019 in Klonkt for src/services
- 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)
- Location:
- src/services
- Files:
-
- 3 edited
-
ActivityPubService.js (modified) (2 diffs)
-
ThemeService.js (modified) (2 diffs)
-
i18n.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r5d0d3f35 r495b019 20 20 import path from 'path'; 21 21 import db, { NU_ISO, isoSql } from '../config/database.js'; 22 import ThemeService from './ThemeService.js'; 22 23 import HtmlSanitizerService from './HtmlSanitizerService.js'; 23 24 import AudioEmbedService from './AudioEmbedService.js'; … … 385 386 // (guardianship module owns these). 386 387 ...Guardianship.guardianshipActorProps(id, site.slug), 388 // Het thema dat de beheerder koos (experimental/theme). Shaer haalt het hier 389 // op in plaats van er zelf een te kiezen: één keuze kleedt de site en de app. 390 // Alleen als er een gekozen is; zonder staat er niets en houdt elke client 391 // zijn eigen standaard. 392 ...ThemeService.layoutActorProp(base, site), 387 393 // C2S clients (Shaer apps) discover auth + upload here — no hardcoded paths. 388 394 // All four are ActivityPub-spec `endpoints` terms. Dynamic client registration -
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 -
src/services/i18n.js
r5d0d3f35 r495b019 289 289 'asite.theme_light': 'Licht', 290 290 'asite.theme_dark': 'Donker', 291 'asite.layout': 'Lay-out', 292 'asite.layout_top': 'Gewone kop', 293 'asite.layout_hint': 'Hoe de site eruitziet op een breed scherm. Bezoekers kiezen niet; deze keuze geldt voor iedereen. Het beheer houdt altijd de gewone kop.', 291 294 'asite.palette': 'Palet', 292 295 'asite.behavior': 'Gedrag', … … 1299 1302 'asite.theme_light': 'Light', 1300 1303 'asite.theme_dark': 'Dark', 1304 'asite.layout': 'Layout', 1305 'asite.layout_top': 'Top bar', 1306 'asite.layout_hint': 'How the site looks on a wide screen. Visitors do not choose; this applies to everyone. The admin always keeps the top bar.', 1301 1307 'asite.palette': 'Palette', 1302 1308 'asite.behavior': 'Behavior', … … 2308 2314 'asite.theme_light': 'Hell', 2309 2315 'asite.theme_dark': 'Dunkel', 2316 'asite.layout': 'Layout', 2317 'asite.layout_top': 'Kopfleiste', 2318 'asite.layout_hint': 'Wie die Seite auf einem breiten Bildschirm aussieht. Besucher wählen nicht; dies gilt für alle. Die Verwaltung behält immer die Kopfleiste.', 2310 2319 'asite.palette': 'Palette', 2311 2320 'asite.behavior': 'Verhalten',
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)