source: Klonkt/scripts/admin-css.mjs@ abd2093

main
Last change on this file since abd2093 was abd2093, checked in by Robin <roboburr@…>, 3 weeks ago

Beheer: audio en playlists ook op de gedeelde ax-romp

Mijn regressie van de vorige commit: ik haalde de opmaak van de tabbalk
uit media-tabs.ejs en zette hem in de partial, maar audio en playlists
nemen die partial niet op. Hun tabbalk stond er dus wel in de markup en
had geen enkele regel CSS -- vijf kale links, en de pagina zat er niet
meer onder.

Beide pagina's nemen de partial nu op, boven hun eigen style-blok zodat
wat daar blijft staan later komt en dus wint.

En daarmee kon het ontdubbelen alsnog, maar nu met een echte ontleding:
scripts/admin-css.mjs leest de tekens een voor een en kent commentaar,
tekenreeksen en geneste blokken. Dat was nodig -- mijn regex van
vanochtend telde @media-varianten als losse regels en commentaar als
selector, en zat er voor playlists 7 regels naast. Twee fouten zaten ook
in de eerste versie van dit script: /* Mobiel */ voor een @media maakte
het blok onherkenbaar, en geneste regels droegen posities uit hun
binnentekst -- daarmee had het weghalen de verkeerde bytes geknipt.

Weg: 27 regels uit admin-audio, 34 uit admin-playlists. Wat afwijkt
blijft staan en wordt gemeld (audio heeft 10 eigen varianten, playlists
2).

scripts/admin-css-gelijk.mjs is het bewijs: hij rendert elke pagina nu
en zoals hij was, rekent per (media-context, selector) uit wat er
uiteindelijk geldt, en legt de twee naast elkaar. Uitkomst over alle
vijf: geen regel verdwenen, geen regel veranderd, en op audio en
playlists 21 resp. 22 regels erbij -- de tabbalk voorop.

Dat audio en playlists nu ook .ax-grid en .ax-tile meekrijgen die ze niet
gebruiken, is de prijs van een gedeelde partial. Een paar honderd bytes.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 5.6 KB
Line 
1/**
2 * Vergelijkt de ax-*-regels van een beheerpagina met partials/admin-styles.ejs.
3 *
4 * node scripts/admin-css.mjs wat zou er gebeuren (leest alleen)
5 * node scripts/admin-css.mjs --doen haalt de dubbele regels weg
6 *
7 * WAAROM EEN ONTLEDING EN GEEN GREP. De eerste poging (16-8) telde
8 * @media-varianten als losse regels en commentaar als selector, en concludeerde
9 * daaruit dat admin-playlists tien "eigen" regels had die het niet had. Een
10 * verkeerd patroon faalt niet, hij liegt. Dit leest de tekens een voor een: hij
11 * kent commentaar, tekenreeksen en geneste blokken, en houdt bij in welke
12 * @media-context een regel staat -- want .ax-track in een media query is een
13 * ANDERE regel dan .ax-track daarbuiten.
14 *
15 * WAT HIJ WEGHAALT: alleen regels die in dezelfde context dezelfde selector en
16 * exact dezelfde verklaringen hebben als de partial. Wijkt er iets af, dan
17 * blijft hij staan en wordt hij gemeld -- dat is een bewuste afwijking van die
18 * pagina en niet aan een script om op te ruimen. De partial hoort BOVENAAN de
19 * pagina te staan, zodat wat blijft staan later komt en dus wint.
20 */
21import fs from 'node:fs';
22
23/** Splitst CSS in blokken. Geen regex: tekens tellen, met besef van context. */
24function ontleed(css, context = '') {
25 const uit = [];
26 let i = 0, start = 0, diepte = 0, blokStart = -1;
27 const n = css.length;
28 while (i < n) {
29 const c = css[i];
30 // Commentaar overslaan -- ook een { of } daarbinnen telt niet mee.
31 if (c === '/' && css[i + 1] === '*') {
32 const eind = css.indexOf('*/', i + 2);
33 i = eind === -1 ? n : eind + 2;
34 continue;
35 }
36 // Tekenreeksen overslaan (content: "}" bestaat echt).
37 if (c === '"' || c === "'") {
38 const aanhaal = c; i++;
39 while (i < n && css[i] !== aanhaal) { if (css[i] === '\\') i++; i++; }
40 i++;
41 continue;
42 }
43 if (c === '{') {
44 if (diepte === 0) blokStart = i;
45 diepte++; i++; continue;
46 }
47 if (c === '}') {
48 diepte--;
49 if (diepte === 0) {
50 // Commentaar ERAF voor we kijken wat voor blok dit is. Stond dit na de
51 // controle, dan werd `/* Mobiel */ @media (...)` niet als media query
52 // herkend en telde het hele blok als een gewone regel met een
53 // krankzinnige selector -- en de regels erbinnen werden nooit
54 // vergeleken. De eerste versie deed dat, en het viel alleen op omdat de
55 // uitvoer "@media (max-width: 480px)" als selector noemde.
56 const kop = norm(schoon(css.slice(start, blokStart)));
57 const lijf = css.slice(blokStart + 1, i);
58 if (kop.startsWith('@media') || kop.startsWith('@supports')) {
59 // Genest: de binnenkant is zelf weer een regelverzameling. De posities
60 // die daaruit komen zijn RELATIEF aan lijf, dus schuif ze naar deze
61 // tekst op -- anders knipt het weghalen straks de verkeerde bytes,
62 // en dat is het soort fout dat een bestand stilletjes sloopt.
63 const verschuif = blokStart + 1;
64 for (const r of ontleed(lijf, kop)) {
65 uit.push({ ...r, van: r.van + verschuif, tot: r.tot + verschuif });
66 }
67 } else {
68 uit.push({
69 context,
70 selector: kop,
71 verklaringen: normVerklaringen(lijf),
72 van: start, tot: i + 1,
73 });
74 }
75 start = i + 1;
76 }
77 i++; continue;
78 }
79 i++;
80 }
81 return uit;
82}
83
84const norm = (s) => s.replace(/\s+/g, ' ').trim();
85/** Commentaar voor een selector hoort bij de selector niet. */
86const schoon = (s) => s.replace(/\/\*[\s\S]*?\*\//g, ' ');
87/** Verklaringen als gesorteerde verzameling: volgorde binnen een blok telt niet. */
88function normVerklaringen(lijf) {
89 return schoon(lijf).split(';').map((d) => norm(d)).filter(Boolean).sort().join('; ');
90}
91
92function styleBlok(bestand) {
93 const tekst = fs.readFileSync(bestand, 'utf8');
94 const m = /<style>([\s\S]*?)<\/style>/.exec(tekst);
95 return m ? { tekst, css: m[1], van: m.index + '<style>'.length } : null;
96}
97
98const PARTIAL = 'src/views/partials/admin-styles.ejs';
99const PAGINAS = process.argv.filter((a) => a.endsWith('.ejs') && a !== PARTIAL);
100const doen = process.argv.includes('--doen');
101
102const gedeeld = new Map();
103for (const r of ontleed(styleBlok(PARTIAL).css)) {
104 gedeeld.set(`${r.context}||${r.selector}`, r.verklaringen);
105}
106console.log(`partial: ${gedeeld.size} regels\n`);
107
108for (const pad of PAGINAS) {
109 const blok = styleBlok(pad);
110 if (!blok) { console.log(`${pad}: geen style-blok`); continue; }
111 const regels = ontleed(blok.css);
112 const dubbel = [], anders = [], eigen = [];
113 for (const r of regels) {
114 const sleutel = `${r.context}||${r.selector}`;
115 if (!gedeeld.has(sleutel)) eigen.push(r);
116 else if (gedeeld.get(sleutel) === r.verklaringen) dubbel.push(r);
117 else anders.push(r);
118 }
119 console.log(`${pad}`);
120 console.log(` ${dubbel.length} identiek aan de partial -> weg`);
121 console.log(` ${anders.length} zelfde selector, ANDERE waarde -> blijft staan`);
122 for (const r of anders) console.log(` ${r.context ? r.context + ' ' : ''}${r.selector}`);
123 console.log(` ${eigen.length} alleen op deze pagina -> blijft staan`);
124
125 if (doen && dubbel.length) {
126 // Van achter naar voren knippen, anders schuiven de posities.
127 let css = blok.css;
128 for (const r of [...dubbel].sort((a, b) => b.van - a.van)) {
129 css = css.slice(0, r.van) + css.slice(r.tot);
130 }
131 css = css.replace(/\n{3,}/g, '\n\n');
132 const nieuw = blok.tekst.slice(0, blok.van) + css + blok.tekst.slice(blok.van + blok.css.length);
133 fs.writeFileSync(pad, nieuw);
134 console.log(' geschreven');
135 }
136 console.log();
137}
Note: See TracBrowser for help on using the repository browser.