diff options
| author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-23 09:26:19 +0200 |
|---|---|---|
| committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-23 09:26:19 +0200 |
| commit | 18f85ced396b6eadfe0823e97ac6ed944d593f58 (patch) | |
| tree | b0e72020e51ea40506d0419ff5add504f87080a7 /src/main/java | |
| parent | 1db8201439680474bf7d0b98721af8011e67d494 (diff) | |
Die Synopse ergeht fortan als Formblatt ÄG-2
Seit die Aufgabeseite das Gewand eines amtlichen Formblatts trägt, sah man
beim Öffnen der Synopse zwei Dokumente zweier Häuser: dort Groteske, Briefkopf
und Amtsrot, hier Antiqua auf Weiß ohne Kopf und ohne Fuß. Sie treten nunmehr
als Vorgang und Erzeugnis desselben Hauses auf — der Antrag als ÄG-1, die
darauf ergehende Synopse als ÄG-2.
Der Kopf trägt denselben Aufbau: links die ausstellende Stelle, rechts die
Kennung mit Formblatt, Stammgesetz, Erstellungstag und Geltung, bei Entwürfen
zusätzlich der Stand. Die Felder des Spaltenkopfes und die Überschriften der
beiden Sonderabschnitte treten als schwarze Balken auf wie die Legende des
Feldblocks. Jede Norm trägt im Steg links eine Randziffer, wie der Vordruck die
Feldziffer; sie macht die Abschnitte zugleich zitierbar. Die alte Fassung liegt
auf dem Kastenton wie ein Durchschlag, die neue auf Bogenweiß.
Die Schrift bleibt geteilt: Der Vordruck steht in der Groteske, der Wortlaut
der Normen in der Antiqua. Amtskopf und Vorschrift sind zweierlei, im
Gesetzblatt wie hier. Dass das Farbgerüst zweimal geführt wird, ist
unvermeidlich und nun an beiden Stellen vermerkt — die Synopse wird aus einem
blob:-Verweis geöffnet und kann kein fremdes Stilblatt einbinden.
Drei Mängel traten dabei zutage. Der Spaltenkopf klebte innerhalb des
Kopfblocks und verschwand deshalb beim Blättern alsbald; er steht jetzt mit der
Gegenüberstellung in einem eigenen Block. Auf schmalen Geräten standen zwei
Spalten Gesetzestext nebeneinander; unterhalb von sechzig Halbgevierten wird
gestapelt, und jede Spalte beschriftet sich dort selbst. Und die Druckblöcke
beider Stilblätter setzten nur einen Teil des Farbgerüstes zurück — aus einem
dunkel eingestellten Browser heraus wäre die alte Spalte als schwarzer Block
aus dem Drucker gekommen.
Geprüft an zwei wirklichen Läufen der Befehlszeilenfassung: UWG mit dem
Dritten Änderungsgesetz (19 Befehle, 0 manuell, 6 Normen) und AGG mit dem
Regierungsentwurf (23 Befehle, 1 manuell, 14 Normen, Entwurfsfassung), beide
hell und dunkel, breit und schmal, dazu die Druckvorschau; reuse lint meldet
154/154, mvnw test dreihundertsiebenundzwanzig Testfälle ohne Fehler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: I76d1f5f14638700d695a7ffe7a6a5933bee10611
Diffstat (limited to 'src/main/java')
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java | 346 |
1 files changed, 297 insertions, 49 deletions
diff --git a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java index 91f5574..09a60de 100644 --- a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java +++ b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java @@ -37,34 +37,60 @@ public final class HtmlRenderer { rendereKopf(sb, synopse, quelleBeschreibung, entwurfsfassung); + // Der Spaltenkopf steht mit der Gegenüberstellung in einem Block: Sein Klebebereich reicht + // damit über alle Normen, endet aber vor dem Abschnitt „Manuell prüfen“, der keine Spalten + // hat und über dem er nur im Wege stünde. + sb.append("<div class=\"gegenueberstellung\">\n"); + sb.append("<div class=\"spaltenkopf\"><div>Alte Fassung</div><div>Neue Fassung</div></div>\n"); + rendereGliederungsAenderungen(sb, synopse); for (var eintrag : synopse.eintraege()) { rendereEintrag(sb, eintrag); } + sb.append("</div>\n"); + rendereManuellZuPruefen(sb, synopse); + rendereFuss(sb); + sb.append("</body>\n</html>\n"); return sb.toString(); } + /** + * Der Briefkopf des Vordrucks ÄG-2, im Aufbau derselbe wie auf der Aufgabeseite: links die + * ausstellende Stelle, rechts die Kennung; darunter der Vorspann mit Herkunft und Statistik. + */ private static void rendereKopf( StringBuilder sb, Synopse synopse, String quelle, boolean entwurfsfassung) { - sb.append("<header>\n<h1>Synopse: ").append(esc(synopse.alt().jurabk())).append("</h1>\n"); + sb.append("<header class=\"briefkopf\">\n<div class=\"dienststelle\">\n"); + sb.append("<p class=\"langname\">").append(LANGNAME).append("</p>\n"); + sb.append("<h1>Synopse: ").append(esc(synopse.alt().jurabk())).append("</h1>\n"); + if (synopse.alt().langue() != null) { + sb.append("<p class=\"langue\">").append(esc(synopse.alt().langue())).append("</p>\n"); + } + sb.append("</div>\n<dl class=\"kennung\">\n"); + sb.append("<dt>Formblatt</dt><dd>ÄG‑2</dd>\n"); + sb.append("<dt>Stammgesetz</dt><dd>") + .append(esc(synopse.alt().jurabk())) + .append("</dd>\n<dt>Erstellt</dt><dd>") + .append(LocalDate.now()) + .append("</dd>\n"); + if (entwurfsfassung) { + sb.append("<dt>Stand</dt><dd>Entwurfsfassung</dd>\n"); + } + sb.append("<dt>Geltung</dt><dd>nichtamtlich</dd>\n"); + sb.append("</dl>\n</header>\n"); + + sb.append("<div class=\"vorgang\">\n"); if (entwurfsfassung) { sb.append( "<p class=\"entwurfshinweis\">Entwurfsfassung — nicht geltendes Recht. Die neue Fassung" + " gibt den Stand des Gesetzgebungsverfahrens wieder.</p>\n"); } - if (synopse.alt().langue() != null) { - sb.append("<p class=\"langue\">").append(esc(synopse.alt().langue())).append("</p>\n"); - } - sb.append("<p class=\"quelle\">") - .append(esc(quelle)) - .append(" — erstellt am ") - .append(LocalDate.now()) - .append(" mit ÄndGgner</p>\n"); + sb.append("<p class=\"quelle\">Grundlage: ").append(esc(quelle)).append("</p>\n"); long geaendert = synopse.eintraege().stream() .filter(e -> e.art() != Synopse.Aenderungsart.UNVERAENDERT) @@ -74,10 +100,27 @@ public final class HtmlRenderer { .append(" geänderte Normen, ") .append(synopse.manuellZuPruefen().size()) .append(" manuell zu prüfende Befehle</p>\n"); - sb.append("<div class=\"spaltenkopf\"><div>Alte Fassung</div><div>Neue Fassung</div></div>\n"); - sb.append("</header>\n"); + sb.append("</div>\n"); } + /** + * Der Fußsteg. Der Vorbehalt gehört gerade hierher und nicht bloß auf die Aufgabeseite: Die + * Synopse wird ausgedruckt und weitergereicht, das Formular nicht. + */ + private static void rendereFuss(StringBuilder sb) { + sb.append("<footer>\n<span>Erstellt am ") + .append(LocalDate.now()) + .append(" mit ÄndGgner</span>\n") + .append( + "<span class=\"nebenquelle\">Ohne Gewähr; maßgeblich ist allein die amtliche" + + " Verkündung im jeweiligen Gesetz- oder Verordnungsblatt.</span>\n") + .append("</footer>\n"); + } + + private static final String LANGNAME = + "Nichtamtliche Zentralstelle für die maschinelle Fortschreibung von Stammgesetzen anhand" + + " von Änderungsvorschriften des Bundes und der Länder"; + private static void rendereGliederungsAenderungen(StringBuilder sb, Synopse synopse) { if (synopse.gliederungsAenderungen().isEmpty()) { return; @@ -208,76 +251,200 @@ public final class HtmlRenderer { private static final String CSS = """ - /* Grundgerüst schwarzweiß wie ein Gesetzblatt; Farbe trägt allein die Änderung: - rot heißt weg, grün heißt hinzu. Damit die Aussage auch im Schwarzweiß-Ausdruck - und bei Farbfehlsichtigkeit erhalten bleibt, tritt zur Farbe stets die Form — - durchgestrichen gegen unterstrichen — sowie die Spalte, in der sie steht. */ + /* Der Vordruck ÄG-2. Das Farbgerüst ist dasselbe wie in + src/main/resources/eu/mulk/aendggner/web/style.css und wird hier ein zweites Mal + geführt: Diese Datei wird aus einem blob:-Verweis geöffnet und im Befehlszeilen- + betrieb als einzelne Datei abgelegt; sie kann kein fremdes Stilblatt einbinden. + Wer dort etwas ändert, ändere es hier mit. + + Die Schrift ist geteilt: Der Vordruck — Briefkopf, Kennung, Überschriften, + Spaltenköpfe, Kästchen, Fußsteg — steht in der Groteske, der Wortlaut der Normen + in der Antiqua. Amtskopf und Vorschrift sind zweierlei, im Gesetzblatt wie hier. + + Farbe trägt allein die Änderung: rot heißt weg, grün heißt hinzu. Damit die + Aussage auch im Schwarzweiß-Ausdruck und bei Farbfehlsichtigkeit erhalten bleibt, + tritt zur Farbe stets die Form — durchgestrichen gegen unterstrichen — sowie die + Spalte, in der sie steht. */ :root { color-scheme: light dark; --fg: #111; --bg: #fff; - --rand: #bbb; - --dezent: #595959; + --papier: #f3f1ea; + --muted: #595959; + --raster: #d6d2c6; + --kasten: #f0ede4; + --amt: #8b0f21; --del-bg: #ffd7d7; --del-fg: #8b0000; --ins-bg: #d7f5d7; --ins-fg: #005f00; + --grotesk: system-ui, "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --antiqua: Georgia, "Times New Roman", serif; } @media (prefers-color-scheme: dark) { :root { --fg: #e6e6e6; - --bg: #141414; - --rand: #4a4a4a; - --dezent: #a0a0a0; + --bg: #1c1c1c; + --papier: #0f0f0f; + --muted: #a0a0a0; + --raster: #3c3c3c; + --kasten: #242424; + --amt: #e79aa4; --del-bg: #5a1f1f; --del-fg: #ffb3b3; --ins-bg: #1f4a1f; --ins-fg: #b3ffb3; } } + * { box-sizing: border-box; } + /* Die Unterlage endet nie, auch wenn der Bogen kürzer ist als das Fenster. */ + html { background: var(--papier); } body { - font-family: Georgia, "Times New Roman", serif; - line-height: 1.45; + counter-reset: norm; + font-family: var(--grotesk); + line-height: 1.5; max-width: 90rem; - margin: 0 auto; - padding: 1rem 2rem; + margin: 2rem auto 3rem; + padding: 2rem 2.25rem 2.5rem; + border: 1px solid var(--fg); color: var(--fg); background: var(--bg); } - header h1 { margin-bottom: 0.2rem; } - .langue { font-style: italic; margin-top: 0; } - .quelle, .statistik, .gliederung, .ursachen { color: var(--dezent); font-size: 0.9rem; } + /* Auf schmalen Geräten wäre der Bogenrand nur ein Verlust an Satzbreite. */ + @media (max-width: 48rem) { + body { margin: 0; padding: 1.25rem 1rem 2rem; border: none; } + } + + /* Briefkopf und Doppellinie wie auf der Aufgabeseite: kräftig in Amtsrot, dünn in + Rasterfarbe, wobei die zweite Linie der Oberrand des Vorspanns ist. */ + .briefkopf { + display: grid; + grid-template-columns: 1fr auto; + gap: 1rem 2rem; + align-items: end; + padding-bottom: 0.7rem; + border-bottom: 3px solid var(--amt); + } + .langname { + margin: 0 0 0.3rem; + max-width: 30rem; + font-size: 0.75rem; + letter-spacing: 0.05em; + text-transform: uppercase; + line-height: 1.35; + color: var(--muted); + } + .dienststelle h1 { + margin: 0; + font-size: 1.9rem; + letter-spacing: 0.06em; + line-height: 1.1; + } + .langue { margin: 0.35rem 0 0; font-style: italic; color: var(--muted); } + .kennung { + display: grid; + grid-template-columns: auto auto; + gap: 0 0.6rem; + margin: 0; + padding: 0.4rem 0.6rem; + border: 1px solid var(--raster); + font-size: 0.75rem; + line-height: 1.5; + } + .kennung dt { letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); } + .kennung dd { margin: 0; font-weight: bold; color: var(--amt); white-space: nowrap; } + /* Gestapelt zöge die Kennung sonst über die ganze Breite und sähe aus wie ein Kasten + des Satzspiegels statt wie ein Stempel am Kopfrand. */ + @media (max-width: 34rem) { + .briefkopf { grid-template-columns: 1fr; align-items: start; } + .kennung { justify-self: start; } + } + + .vorgang { margin-top: 3px; padding-top: 1rem; border-top: 1px solid var(--raster); } + .quelle, .statistik, .gliederung, .ursachen { + color: var(--muted); + font-size: 0.85rem; + margin: 0.2rem 0; + } .entwurfshinweis { - border: 1px solid var(--rand); + margin: 0 0 0.9rem; + padding: 0.9rem 1.1rem; + border: 1px solid var(--raster); border-left: 4px solid var(--del-fg); - padding: 0.5rem 0.75rem; - margin: 0.6rem 0; - font-size: 0.95rem; + background: var(--kasten); + font-size: 0.9rem; } + + /* Der Spaltenkopf steht mit der Gegenüberstellung in einem Block und nicht mehr im + Kopfblock: Nur so reicht sein Klebebereich über alle Normen und nicht bloß über den + Vorspann. Seine beiden Felder tragen die Form der Legende des Antragsvordrucks. */ .spaltenkopf { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; - font-weight: bold; - border-bottom: 2px solid var(--rand); - padding: 0.5rem 0; position: sticky; top: 0; - background: inherit; + z-index: 1; + margin-top: 1.25rem; + padding: 0.6rem 0 0.6rem 3rem; + background: var(--bg); } - section.norm { border-bottom: 1px solid var(--rand); padding: 0.7rem 0; } - section.norm h2 { font-size: 1.15rem; margin: 0.3rem 0; } + .spaltenkopf div { + padding: 0.3rem 0.8rem; + background: var(--fg); + color: var(--bg); + font-size: 0.8rem; + font-weight: bold; + letter-spacing: 0.08em; + text-transform: uppercase; + } + + /* Der Steg links trägt die Randziffer, wie im Vordruck die Feldziffer; sie macht die + Abschnitte überdies zitierbar. */ + section { position: relative; padding-left: 3rem; } + section.norm { + counter-increment: norm; + padding-top: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--raster); + } + section.norm::before { + content: counter(norm); + position: absolute; + top: 1.15rem; + left: 0; + width: 1.5rem; + height: 1.5rem; + border: 1px solid var(--amt); + color: var(--amt); + font-size: 0.8rem; + font-weight: bold; + line-height: 1.4rem; + text-align: center; + } + section.norm h2 { font-size: 1.05rem; margin: 0 0 0.5rem; letter-spacing: 0.02em; } + .vergleich { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; } + /* Die alte Fassung liegt auf dem Kastenton wie ein Durchschlag, die neue auf + Bogenweiß; beim Springen zwischen den Spalten ist damit ohne Hinsehen klar, wo man + steht. Die Hinterlegungen der Änderungen heben sich von beiden Gründen ab. */ .vergleich > div { + padding: 0.75rem 0.9rem; + border: 1px solid var(--raster); white-space: pre-wrap; overflow-wrap: anywhere; + font-family: var(--antiqua); font-size: 0.95rem; + line-height: 1.45; } + .vergleich > .alt { background: var(--kasten); } + .vergleich > .neu { background: var(--bg); } + del, .alt del { background: var(--del-bg); color: var(--del-fg); @@ -288,31 +455,112 @@ public final class HtmlRenderer { color: var(--ins-fg); text-decoration: underline; } - .leer { color: var(--dezent); font-style: italic; } + .leer { color: var(--muted); font-style: italic; } + /* Kästchen in Versalien; die Farbe folgt derselben Regel wie im Diff. Beschriftet - sind sie ohnehin, sodass die Farbe hier nur bestätigt, was dasteht. */ + sind sie ohnehin, sodass die Farbe hier nur bestätigt, was dasteht. Der Rahmen + steht auch dort, wo die Hinterlegung schwach wirkt. */ .badge { - font-family: system-ui, sans-serif; + font-family: var(--grotesk); font-size: 0.7rem; font-weight: normal; text-transform: uppercase; letter-spacing: 0.06em; - border: 1px solid transparent; + border: 1px solid var(--raster); padding: 0.1rem 0.55rem; vertical-align: middle; + white-space: nowrap; + } + .neu-badge { background: var(--ins-bg); color: var(--ins-fg); border-color: var(--ins-fg); } + .aufgehoben-badge { + background: var(--del-bg); + color: var(--del-fg); + border-color: var(--del-fg); + } + .geaendert-badge { border-color: var(--raster); color: var(--muted); } + + /* Die Abschnittsüberschriften treten als Balken auf wie die Legende des Feldblocks. */ + section.gliederung-aenderungen > h2, section.manuell > h2 { + display: inline-block; + margin: 0 0 0.9rem; + padding: 0.3rem 0.8rem; + background: var(--fg); + color: var(--bg); + font-size: 0.8rem; + font-weight: bold; + letter-spacing: 0.08em; + text-transform: uppercase; + } + section.gliederung-aenderungen { padding-top: 1.25rem; } + section.manuell { margin-top: 2rem; padding-top: 1.5rem; border-top: 3px solid var(--amt); } + section.manuell h3 { + margin: 1.25rem 0 0.5rem; + font-size: 0.8rem; + letter-spacing: 0.06em; + text-transform: uppercase; } - .neu-badge { background: var(--ins-bg); color: var(--ins-fg); } - .aufgehoben-badge { background: var(--del-bg); color: var(--del-fg); } - .geaendert-badge { border-color: var(--rand); color: var(--dezent); } - section.manuell { margin-top: 2rem; } section.manuell li { margin-bottom: 0.7rem; } - .originaltext { color: var(--dezent); font-size: 0.85rem; } + .originaltext { font-family: var(--antiqua); color: var(--muted); font-size: 0.85rem; } + + footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 0.5rem 1.5rem; + margin-top: 2.5rem; + padding-top: 0.9rem; + border-top: 1px solid var(--fg); + font-size: 0.8125rem; + color: var(--muted); + } + + /* Zwei Spalten Gesetzestext nebeneinander sind auf dem Telefon nicht zu lesen. Gestapelt + trägt der Spaltenkopf nichts mehr; deshalb beschriftet sich dort jede Spalte selbst. */ + @media (max-width: 60rem) { + .spaltenkopf { display: none; } + .vergleich { grid-template-columns: 1fr; } + section { padding-left: 2.25rem; } + .vergleich > div::before { + display: block; + margin-bottom: 0.4rem; + font-family: var(--grotesk); + font-size: 0.7rem; + font-weight: bold; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + } + .vergleich > .alt::before { content: "Alte Fassung"; } + .vergleich > .neu::before { content: "Neue Fassung"; } + } + + /* Gedruckt wird stets auf weißes Papier, auch aus einem dunkel eingestellten Browser + heraus: prefers-color-scheme gilt im Druck fort. Deshalb ist hier das ganze + Farbgerüst zurückzusetzen und nicht nur ein Teil davon — sonst käme die alte Spalte + als schwarzer Block aus dem Drucker. */ @media print { - .vergleich { break-inside: avoid; } + :root { + --fg: #000; + --bg: #fff; + --papier: #fff; + --muted: #333; + --raster: #999; + --kasten: #f2f0ea; + --amt: #000; + --del-bg: #ffd7d7; + --del-fg: #8b0000; + --ins-bg: #d7f5d7; + --ins-fg: #005f00; + } + body { margin: 0; padding: 0; max-width: none; border: none; font-size: 10pt; } + .spaltenkopf { position: static; } + .vergleich, section.norm { break-inside: avoid; } /* Ohne diese Festlegung wirft der Druck die Hinterlegung weg. Durchstreichung und Unterstreichung überstehen ihn ohnehin und tragen die Aussage auch dort, - wo schwarzweiß gedruckt wird. */ - del, ins, .badge, .entwurfshinweis { + wo schwarzweiß gedruckt wird; der Spaltenbalken und der Durchschlagston der + alten Fassung aber haben keine solche zweite Gestalt. */ + del, ins, .badge, .entwurfshinweis, .spaltenkopf div, .vergleich > .alt, + section.gliederung-aenderungen > h2, section.manuell > h2 { -webkit-print-color-adjust: exact; print-color-adjust: exact; } |
