/* STEHT DAS CSS AUCH AUF DER SEITE, DIE ES BENUTZT? ENTSTANDEN AUS EINEM ECHTEN FEHLER (05.09.2026). Die Teilnehmerwahl im Kalender benutzte `.wahl__knopf` und `.wahl__rolle`. Beide sind sauber definiert -- in dateien.css. Und kalender.html bindet dateien.css nicht ein. Ohne `display:inline-flex` und `gap` stehen zwei eben direkt nebeneinander. Auf Filipes Bildschirm stand deshalb: DogfatherDogFather VanVanDogFather CigdemManager Vierzig Prüfungen zur Teilnehmerwahl waren grün. Sie prüften, dass die richtigen Personen ankommen, dass Fremde weggefiltert werden, dass die Sichtbarkeit stimmt -- alles richtig, und alles am Problem vorbei. KEINE hat gefragt, ob das Ergebnis auch aussieht wie gedacht. Das ist die dritte Sorte Fehler aus der Projektnotiz: nicht der übersprungene Test, nicht der grüne, der das Falsche prüft, sondern die Frage, die niemand gestellt hat. WAS HIER GEMESSEN WIRD Für jede Workspace-Seite: 1. Welche Stylesheets bindet sie ein? (rekursiv über @import) 2. Welche Klassen vergibt sie -- im HTML *und* in ihrem eigenen JavaScript (className, classList.add, setAttribute('class')). 3. Ist jede davon in einem der eingebundenen Stylesheets definiert? WAS BEWUSST NICHT GEMESSEN WIRD Klassen, die es nur zum Anfassen gibt (`js-`), reine Datenanker und Klassen aus gemeinsamen Bausteinen, die jede Seite lädt. Und die Gegenrichtung ("CSS ohne Benutzer") gehört nicht hierher: Ungenutztes CSS ist Ballast, kein Fehler. DRITTER AUSGANG: Findet die Prüfung keine Seiten oder keine Klassen, ist das kein "bestanden" -- dann hat sie nichts gemessen und sagt es. GEGENPROBE am Ende: Eine erfundene Klasse muss auffallen, und eine echte darf es nicht. */ import { readFileSync, readdirSync, existsSync } from "node:fs"; import { join, dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const HIER = dirname(fileURLToPath(import.meta.url)); const WURZEL = resolve(HIER, ".."); const WS = join(WURZEL, "workspace"); let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; /* ---------- Welche Stylesheets bindet eine Seite ein? -------------------- */ /** Liest ein Stylesheet und folgt seinen @import-Zeilen. */ function cssMitImporten(pfad, gesehen = new Set()) { const echt = resolve(pfad); if (gesehen.has(echt) || !existsSync(echt)) return ""; gesehen.add(echt); const text = readFileSync(echt, "utf8"); let alles = text; for (const m of text.matchAll(/@import\s+(?:url\()?["']([^"')]+)["']\)?/g)) { alles += "\n" + cssMitImporten(join(dirname(echt), m[1].split("?")[0]), gesehen); } return alles; } /** Alle Klassennamen, die ein CSS-Text definiert. */ function definierteKlassen(css) { const raus = new Set(); /* Erst Kommentare weg: In diesem Haus stehen lange Erklärungen im CSS, und dort kommen Klassennamen als Fließtext vor ("wie .wahl__knopf in dateien.css"). Die zählen nicht als Definition -- sonst würde ausgerechnet der Kommentar, der den Fehler beschreibt, ihn verdecken. */ const ohne = css.replace(/\/\*[\s\S]*?\*\//g, " "); for (const m of ohne.matchAll(/\.(-?[A-Za-z_][\w-]*)/g)) raus.add(m[1]); return raus; } /* ---------- Welche Klassen vergibt eine Seite? --------------------------- */ /** Klassen aus dem HTML. */ function klassenAusHtml(html) { const raus = new Set(); const ohneKommentar = html.replace(//g, " "); for (const m of ohneKommentar.matchAll(/\bclass\s*=\s*["']([^"']+)["']/g)) { for (const k of m[1].split(/\s+/)) if (k) raus.add(k); } return raus; } /** Klassen aus JavaScript. Nur die Formen, die dieses Haus benutzt -- * ein vollständiger Parser wäre hier Aufwand ohne Ertrag. */ function klassenAusJs(js) { const raus = new Set(); const ohne = js.replace(/\/\*[\s\S]*?\*\//g, " "); const muster = [ /\.className\s*=\s*['"]([^'"]+)['"]/g, /setAttribute\(\s*['"]class['"]\s*,\s*['"]([^'"]+)['"]/g, /class\s*=\s*\\?["']([^"'\\]+)/g, // in Template-Literalen ]; for (const m of muster) { for (const t of ohne.matchAll(m)) { for (const stueck of t[1].split(/['",\s]+/)) { const k = stueck.trim(); /* Nur echte Klassennamen -- keine Variablen, keine Ausdrücke. */ if (k && /^-?[A-Za-z_][\w-]*$/.test(k)) raus.add(k); } } } /* classList.add() BRAUCHT eine eigene Behandlung. Der erste Entwurf nahm den ganzen Klammerinhalt und zerlegte ihn an Anführungszeichen und Kommas. Bei classList.add(i === 0 ? 'marke__haupt' : 'marke__leise') fiel dabei `i` als angebliche Klasse heraus -- und die Prüfung meldete auf sechzehn Seiten eine fehlende Klasse `.i`, die es nie gab. Eine Warnung, die falsch ist, ist schlimmer als keine: Sie kostet Zeit und wird beim nächsten Mal überlesen. Deshalb werden hier NUR die Zeichenketten gelesen, nie der Ausdruck drumherum. */ for (const t of ohne.matchAll(/classList\.(?:add|remove|toggle)\(([^)]*)\)/g)) { for (const s of t[1].matchAll(/['"]([^'"]+)['"]/g)) { for (const k of s[1].trim().split(/\s+/)) { if (k && /^-?[A-Za-z_][\w-]*$/.test(k)) raus.add(k); } } } return raus; } /** Welche eigenen Skripte bindet die Seite ein? */ function skripteVon(html) { const raus = []; for (const m of html.matchAll(/]+src\s*=\s*["']([^"']+)["']/g)) { const p = m[1].split("?")[0]; if (/^https?:/.test(p)) continue; // fremde Skripte nicht raus.push(join(WS, p.replace(/^\//, "").replace(/^workspace\//, ""))); } return raus; } function stylesheetsVon(html) { const raus = []; for (const m of html.matchAll(/]+rel\s*=\s*["']stylesheet["'][^>]*>/g)) { const h = /href\s*=\s*["']([^"']+)["']/.exec(m[0]); if (!h || /^https?:/.test(h[1])) continue; raus.push(join(WS, h[1].split("?")[0].replace(/^\//, "").replace(/^workspace\//, ""))); } return raus; } /* Klassen, die absichtlich kein CSS haben. Jede Ausnahme braucht einen Grund -- eine Liste ohne Begründung wächst, bis sie alles enthält. */ const OHNE_CSS = new Set([ "leise", // Grundstil, steht in gate.css als Elementregel ]); const OHNE_CSS_PRAEFIX = ["js-", "u-"]; // reine Anfasser, nie gestaltet /* ======================================================================= Die Messung ======================================================================= */ console.log("\n=== Ist jede benutzte Klasse auf ihrer Seite auch definiert? ==="); const seiten = readdirSync(WS).filter((n) => n.endsWith(".html")); /* DIE ANZAHL GEHOERT IN DIE BEDINGUNG, nicht nur in den Meldetext. Findet die Prüfung keine Seiten, hat sie nichts gemessen -- und "0 von 0 Klassen fehlen" wäre grün und wertlos. */ ok(seiten.length >= 10, `${seiten.length} Workspace-Seiten gefunden`); /* ALLES CSS DES HAUSES -- für die entscheidende Unterscheidung. Eine Klasse ohne CSS ist zweierlei, und nur eines davon ist ein Fehler: (a) NIRGENDS gestaltet. Ein reiner Struktur-Anker wie `.k-wdh__ende`, der nur ein
benennt. Kein Fehler -- sonst müsste man jedem Container Deko andichten. (b) Gestaltet, aber in einer Datei, die DIESE SEITE NICHT LÄDT. Das ist der Fehler von heute: Jemand hat es gestaltet, es sieht im Editor richtig aus, und auf der Seite kommt nichts an. Niemandem fällt es auf, weil das CSS ja existiert. Nur (b) schlägt an. */ const ALLE_CSS = definierteKlassen( readdirSync(join(WS, "assets/css")).filter((n) => n.endsWith(".css")) .map((n) => readFileSync(join(WS, "assets/css", n), "utf8")).join("\n")); let geprueft = 0; const luecken = []; // (b) -- gestaltet, aber nicht hier const ungestaltet = []; // (a) -- nur zur Ansicht for (const seite of seiten) { const html = readFileSync(join(WS, seite), "utf8"); const css = stylesheetsVon(html).map((p) => cssMitImporten(p)).join("\n"); const definiert = definierteKlassen(css); const benutzt = klassenAusHtml(html); for (const skript of skripteVon(html)) { if (!existsSync(skript)) continue; for (const k of klassenAusJs(readFileSync(skript, "utf8"))) benutzt.add(k); } for (const k of benutzt) { if (OHNE_CSS.has(k)) continue; if (OHNE_CSS_PRAEFIX.some((p) => k.startsWith(p))) continue; geprueft++; if (definiert.has(k)) continue; if (ALLE_CSS.has(k)) luecken.push(`${seite}: .${k}`); else ungestaltet.push(`${seite}: .${k}`); } } /* Auch hier die Anzahl in die Bedingung: Wären alle Muster kaputt und fänden nichts, stünde sonst "0 Lücken" da -- grün, ohne je eine Klasse angesehen zu haben. */ ok(geprueft > 200, `${geprueft} Klassenverwendungen geprüft`); /* NACH KLASSE GRUPPIERT, nicht nach Seite. Eine fehlende Klasse auf sechzehn Seiten ist EIN Fehler, nicht sechzehn -- und wer sechzehn fast gleiche Zeilen liest, überliest die siebzehnte, die etwas anderes sagt. */ const nachKlasse = new Map(); for (const eintrag of luecken) { const [seite, klasse] = eintrag.split(": ."); if (!nachKlasse.has(klasse)) nachKlasse.set(klasse, []); nachKlasse.get(klasse).push(seite); } const bericht = [...nachKlasse.entries()] .sort((a, b) => b[1].length - a[1].length) .map(([k, s]) => `.${k} — fehlt auf ${s.length} Seite${s.length === 1 ? "" : "n"}` + ` (${s.slice(0, 3).join(", ")}${s.length > 3 ? " …" : ""})`); ok(luecken.length === 0 && geprueft > 200, luecken.length ? `${nachKlasse.size} Klasse${nachKlasse.size === 1 ? "" : "n"} sind gestaltet, aber nicht auf ihrer Seite:` + `\n ${bericht.join("\n ")}` : `jede gestaltete Klasse kommt auch auf ihrer Seite an`); /* Kein Fehler, nur zur Kenntnis: Klassen, die es nirgends gibt. Meist Struktur-Anker; gelegentlich aber ein Tippfehler, der hier auffällt, bevor jemand sich wundert, warum nichts passiert. */ const nurAnker = new Set(ungestaltet.map((e) => e.split(": .")[1])); console.log(` -- ${nurAnker.size} Klassen sind nirgends gestaltet ` + `(Struktur-Anker, kein Fehler): ${[...nurAnker].slice(0, 8).join(", ")}` + (nurAnker.size > 8 ? " …" : "")); /* ======================================================================= GEGENPROBE — die Messung muss beides können ======================================================================= */ console.log("\n=== Gegenprobe ==="); { /* 1. Der echte Fehler von heute, nachgestellt: eine Seite, die eine Klasse benutzt, deren CSS in einem nicht eingebundenen Stylesheet steht. */ const definiert = definierteKlassen(".k-dabei__knopf { color: red; }"); ok(definiert.has("k-dabei__knopf"), "eine definierte Klasse wird erkannt"); ok(!definiert.has("gibt-es-nicht"), "eine erfundene Klasse wird NICHT erkannt"); /* 2. Klassen aus JavaScript werden wirklich mitgelesen -- sonst wäre genau der heutige Fall unsichtbar geblieben, denn die Wahl wird komplett aus JS gebaut und steht in keinem HTML. */ const ausJs = klassenAusJs("b.className = 'k-dabei__knopf';\nx.classList.add('offen');"); ok(ausJs.has("k-dabei__knopf") && ausJs.has("offen"), "Klassen aus className und classList.add werden gefunden"); /* 3. Ein Klassenname, der nur im KOMMENTAR steht, gilt nicht als definiert. Sonst hätte ausgerechnet die Erklärung dieses Fehlers ihn zugedeckt. */ const nurKommentar = definierteKlassen("/* wie .wahl__knopf in dateien.css */\n.echt { color: red; }"); ok(!nurKommentar.has("wahl__knopf") && nurKommentar.has("echt"), "ein Klassenname aus einem Kommentar zählt nicht als Definition"); /* 4. Und der Beweis am echten Bestand: Die Wahl im Kalender muss JETZT gedeckt sein -- vor der Reparatur wäre genau das rot. */ const kal = readFileSync(join(WS, "kalender.html"), "utf8"); const kalCss = definierteKlassen(stylesheetsVon(kal).map((p) => cssMitImporten(p)).join("\n")); const noetig = ["k-dabei", "k-dabei__knopf", "k-dabei__zeichen", "k-dabei__name", "k-dabei__titel", "k-dabei__liste", "k-dabei__gruppe", "k-dabei__haken", "k-dabei__punkt"]; const fehlend = noetig.filter((k) => !kalCss.has(k)); ok(fehlend.length === 0 && noetig.length === 9, fehlend.length ? `im Kalender fehlt CSS für: ${fehlend.join(", ")}` : `alle ${noetig.length} Klassen der Teilnehmerwahl sind in kalender.css definiert`); /* 5. Und die alten Klassen sind wirklich weg -- nicht nur die neuen da. Ein halber Umbau hinterlässt beides. */ const kalJs = readFileSync(join(WS, "assets/js/kalender.js"), "utf8"); ok(!/wahl__knopf|wahl__rolle|wahl__name/.test(kalJs.replace(/\/\*[\s\S]*?\*\//g, " ")), "die alten .wahl__*-Klassen stehen nicht mehr im Kalender-Skript"); /* 6. DIE WICHTIGSTE GEGENPROBE: Unterscheidet die Prüfung die beiden Fälle wirklich? Der ganze Wert hängt daran -- ohne sie könnte die Bedingung "ALLE_CSS.has(k)" verdreht sein, und die Prüfung wäre grün, während sie den echten Fehler durchwinkt. Nachgestellt wird der Fall von heute an einem Miniaturbeispiel: Zwei Klassen, beide ohne CSS auf ihrer Seite -- eine davon woanders gestaltet, die andere nirgends. */ const seitenCss = definierteKlassen(".nur-hier { color: red; }"); const hausCss = definierteKlassen(".nur-hier { color: red; } .woanders { color: blue; }"); const fallB = !seitenCss.has("woanders") && hausCss.has("woanders"); ok(fallB, fallB ? "gestaltet, aber nicht auf dieser Seite → wird als Fehler erkannt" : "MISSLUNGEN — der eigentliche Fehlerfall würde durchgehen"); const fallA = !seitenCss.has("blosser-anker") && !hausCss.has("blosser-anker"); ok(fallA, fallA ? "nirgends gestaltet → gilt als Struktur-Anker, kein Fehler" : "MISSLUNGEN — Struktur-Anker würden fälschlich anschlagen"); /* 7. Und der Fehlalarm, den es schon gab, darf nicht zurückkommen: `classList.add(i === 0 ? 'a' : 'b')` hatte `i` als Klasse gemeldet -- auf sechzehn Seiten. */ const ausAusdruck = klassenAusJs("x.classList.add(i === 0 ? 'marke__haupt' : 'marke__leise');"); ok(!ausAusdruck.has("i") && ausAusdruck.has("marke__haupt") && ausAusdruck.has("marke__leise"), "aus classList.add(bedingung ? 'a' : 'b') werden beide Klassen gelesen, aber nicht die Variable"); } console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`); process.exit(fehler ? 1 : 0);