diff options
Diffstat (limited to 'src/main')
10 files changed, 426 insertions, 50 deletions
diff --git a/src/main/java/eu/mulk/aendggner/AendGgner.java b/src/main/java/eu/mulk/aendggner/AendGgner.java index 60a3775..195a425 100644 --- a/src/main/java/eu/mulk/aendggner/AendGgner.java +++ b/src/main/java/eu/mulk/aendggner/AendGgner.java @@ -113,6 +113,23 @@ public class AendGgner implements Callable<Integer> { + "yet entered into force are listed separately instead of being applied.") private String stichtag; + @Option( + names = "--neufassung", + paramLabel = "<file>", + description = + "Also write the amended base law as canonical plain text (\"-\" = stdout). The file can " + + "be fed back in as the base law of a further amendment act.") + private String neufassung; + + @Option( + names = "--nachfassung", + paramLabel = "<file>", + description = + "Compare the result against the official amended version, norm by norm. Accepts the same " + + "formats as the base law. The report is added to the synopsis; a mismatch yields " + + "exit code 3.") + private Path nachfassung; + public static void main(String... args) { int exitCode = new CommandLine(new AendGgner()).execute(args); System.exit(exitCode); @@ -220,7 +237,14 @@ public class AendGgner implements Callable<Integer> { } } - var ergebnis = Pipeline.erzeugeSynopse(baseFile, patches, artikel, vollstaendig, tag); + var auftrag = + Pipeline.Auftrag.von(baseFile, patches) + .mitArtikel(artikel) + .mitVollstaendig(vollstaendig) + .mitStichtag(tag) + .mitNachfassung(nachfassung == null ? null : Quelle.lies(nachfassung)); + + var ergebnis = Pipeline.erzeugeSynopse(auftrag); if (output.equals("-")) { System.out.println(ergebnis.html()); @@ -229,10 +253,39 @@ public class AendGgner implements Callable<Integer> { log.infof("Synopse nach %s geschrieben.", output); } + if (neufassung != null) { + if (neufassung.equals("-")) { + System.out.print(ergebnis.neufassung()); + } else { + Files.writeString(Path.of(neufassung), ergebnis.neufassung(), StandardCharsets.UTF_8); + log.infof("Neue Fassung nach %s geschrieben.", neufassung); + } + } + System.err.printf( "%d Befehle angewandt, %d manuell zu prüfen, %d geänderte Normen.%n", ergebnis.anzahlAngewandt(), ergebnis.anzahlManuell(), ergebnis.anzahlGeaenderteNormen()); + var abgleich = ergebnis.abgleich(); + if (abgleich != null) { + System.err.println("Abgleich mit der amtlichen Nachfassung: " + abgleich.kurzbericht()); + for (var abweichung : abgleich.abweichungen()) { + System.err.println(" abweichend: " + abweichung.enbez()); + } + for (var fehlend : abgleich.fehlende()) { + System.err.println(" fehlt: " + fehlend); + } + for (var ueberzaehlig : abgleich.ueberzaehlige()) { + System.err.println(" überzählig: " + ueberzaehlig); + } + // Ein eigener Ausgang, damit ein Massenlauf die Abweichung bemerkt, ohne die Ausgabe zu + // lesen. Er geht dem Ausgang 2 vor: Dass die Fassung nicht stimmt, wiegt schwerer als + // dass kein Befehl gegriffen hat — Letzteres wäre ohnehin dessen Ursache. + if (!abgleich.gehtAuf()) { + return 3; + } + } + return ergebnis.anzahlAngewandt() == 0 && ergebnis.anzahlProtokollEintraege() > 0 ? 2 : 0; } diff --git a/src/main/java/eu/mulk/aendggner/Pipeline.java b/src/main/java/eu/mulk/aendggner/Pipeline.java index 9cc47eb..c7b22bf 100644 --- a/src/main/java/eu/mulk/aendggner/Pipeline.java +++ b/src/main/java/eu/mulk/aendggner/Pipeline.java @@ -15,12 +15,15 @@ import eu.mulk.aendggner.aenderung.parse.SuperskriptModus; import eu.mulk.aendggner.aenderung.parse.TextBereiniger; import eu.mulk.aendggner.anwendung.BefehlAnwender; import eu.mulk.aendggner.anwendung.Grund; +import eu.mulk.aendggner.anwendung.Nachfassungsabgleich; import eu.mulk.aendggner.gesetz.Gesetz; import eu.mulk.aendggner.gesetz.Superskript; import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader; import eu.mulk.aendggner.gesetz.land.LandesRechtLoader; +import eu.mulk.aendggner.gesetz.land.LandesRechtTextAusgeber; import eu.mulk.aendggner.synopse.HtmlRenderer; import eu.mulk.aendggner.synopse.SynopseBuilder; +import java.io.IOException; import java.nio.file.Path; import java.time.LocalDate; import java.util.ArrayList; @@ -42,12 +45,82 @@ public final class Pipeline { private Pipeline() {} + /** + * Was ein Auftrag zu erledigen ist. Der Record tritt an die Stelle einer Überladungsleiter: Mit + * jedem weiteren Wahlrecht wuchs die Zahl der Signaturen, und die Aufrufer mussten Angaben + * durchreichen, die sie nichts angehen. Pflicht sind das Stammgesetz und wenigstens ein + * Änderungsdokument; alles Übrige tritt über die {@code mit…}-Methoden hinzu. + * + * @param artikel nur diesen Artikel des Änderungsgesetzes anwenden; {@code null} = alle, deren + * Einleitung das Stammgesetz nennt. + * @param vollstaendig auch unveränderte Normen in die Synopse aufnehmen. + * @param stichtag die Fassung dieses Tages erzeugen: Befehle, die an ihm noch nicht in Kraft + * waren, bleiben unangewandt und werden gesondert ausgewiesen. {@code null} = alle Befehle + * anwenden (dann warnt die Synopse, wenn das Gesetz gestaffelt in Kraft tritt). + * @param nachfassung die amtliche Nachfassung, gegen die das Ergebnis normweise zu halten ist; + * {@code null} = kein Abgleich. + */ + public record Auftrag( + Quelle stammgesetz, + List<Quelle> aenderungsdokumente, + @Nullable String artikel, + boolean vollstaendig, + @Nullable LocalDate stichtag, + @Nullable Quelle nachfassung) { + + public Auftrag { + aenderungsdokumente = List.copyOf(aenderungsdokumente); + } + + public static Auftrag von(Quelle stammgesetz, List<Quelle> aenderungsdokumente) { + return new Auftrag(stammgesetz, aenderungsdokumente, null, false, null, null); + } + + /** Bequemlichkeit für Befehlszeile und Tests; im Browser gibt es keine {@link Path}e. */ + public static Auftrag von(Path stammgesetz, List<Path> aenderungsdokumente) throws IOException { + var quellen = new ArrayList<Quelle>(); + for (var datei : aenderungsdokumente) { + quellen.add(Quelle.lies(datei)); + } + return von(Quelle.lies(stammgesetz), quellen); + } + + public Auftrag mitArtikel(@Nullable String neuerArtikel) { + return new Auftrag( + stammgesetz, aenderungsdokumente, neuerArtikel, vollstaendig, stichtag, nachfassung); + } + + public Auftrag mitVollstaendig(boolean neuVollstaendig) { + return new Auftrag( + stammgesetz, aenderungsdokumente, artikel, neuVollstaendig, stichtag, nachfassung); + } + + public Auftrag mitStichtag(@Nullable LocalDate neuerStichtag) { + return new Auftrag( + stammgesetz, aenderungsdokumente, artikel, vollstaendig, neuerStichtag, nachfassung); + } + + public Auftrag mitNachfassung(@Nullable Quelle neueNachfassung) { + return new Auftrag( + stammgesetz, aenderungsdokumente, artikel, vollstaendig, stichtag, neueNachfassung); + } + } + + /** + * @param neufassung die fortgeschriebene Fassung als kanonischer Klartext. Sie fällt ohnehin an + * und wird deshalb stets mitgegeben: An ihr hängen die Kette (Heft auf Heft) und jede Prüfung + * von außen. + * @param abgleich der normweise Vergleich mit der amtlichen Nachfassung; {@code null}, wenn der + * Auftrag keine nannte. + */ public record Ergebnis( String html, long anzahlAngewandt, long anzahlManuell, int anzahlGeaenderteNormen, - int anzahlProtokollEintraege) {} + int anzahlProtokollEintraege, + String neufassung, + @Nullable Nachfassungsabgleich abgleich) {} /** * Ein eingespeistes Änderungsdokument samt erkannter Art und aufbereitetem Text. @@ -87,44 +160,11 @@ public final class Pipeline { } } - /** Bequemlichkeit für Befehlszeile und Tests; im Browser gibt es keine {@link Path}e. */ - public static Ergebnis erzeugeSynopse( - Path baseFile, List<Path> patches, String artikel, boolean vollstaendig) throws Exception { - return erzeugeSynopse(baseFile, patches, artikel, vollstaendig, null); - } - - public static Ergebnis erzeugeSynopse( - Path baseFile, - List<Path> patches, - String artikel, - boolean vollstaendig, - @Nullable LocalDate stichtag) - throws Exception { - var patchQuellen = new ArrayList<Quelle>(); - for (var patch : patches) { - patchQuellen.add(Quelle.lies(patch)); - } - return erzeugeSynopse(Quelle.lies(baseFile), patchQuellen, artikel, vollstaendig, stichtag); - } - - public static Ergebnis erzeugeSynopse( - Quelle baseFile, List<Quelle> patches, String artikel, boolean vollstaendig) - throws Exception { - return erzeugeSynopse(baseFile, patches, artikel, vollstaendig, null); - } - - /** - * @param stichtag die Fassung dieses Tages erzeugen: Befehle, die an ihm noch nicht in Kraft - * waren, bleiben unangewandt und werden gesondert ausgewiesen. {@code null} = alle Befehle - * anwenden (dann warnt die Synopse, wenn das Gesetz gestaffelt in Kraft tritt). - */ - public static Ergebnis erzeugeSynopse( - Quelle baseFile, - List<Quelle> patches, - String artikel, - boolean vollstaendig, - @Nullable LocalDate stichtag) - throws Exception { + public static Ergebnis erzeugeSynopse(Auftrag auftrag) throws Exception { + var baseFile = auftrag.stammgesetz(); + var patches = auftrag.aenderungsdokumente(); + var artikel = auftrag.artikel(); + var stichtag = auftrag.stichtag(); var altesGesetz = ladeStammgesetz(baseFile); var extraktor = new PatchTextExtraktor(superskriptModus(altesGesetz)); var parser = new AenderungsgesetzParser(); @@ -165,7 +205,20 @@ public final class Pipeline { var gesamtErgebnis = new BefehlAnwender.AnwendungsErgebnis(gesetz, protokoll); var synopse = SynopseBuilder.baue( - altesGesetz, gesamtErgebnis, warnungen, vollstaendig, inkrafttreten, stichtag); + altesGesetz, + gesamtErgebnis, + warnungen, + auftrag.vollstaendig(), + inkrafttreten, + stichtag); + + // Der Abgleich setzt die fertige neue Fassung voraus und tritt deshalb erst hier hinzu. + Nachfassungsabgleich abgleich = null; + if (auftrag.nachfassung() != null) { + abgleich = Nachfassungsabgleich.vergleiche(ladeStammgesetz(auftrag.nachfassung()), gesetz); + synopse = synopse.mitAbgleich(abgleich); + } + var quellenZeile = baseFile.name() + " + " + String.join(" + ", quellen); var html = HtmlRenderer.rendere(synopse, quellenZeile, entwurfsfassung); @@ -174,7 +227,9 @@ public final class Pipeline { gesamtErgebnis.anzahlAngewandt(), gesamtErgebnis.anzahlManuell(), synopse.eintraege().size(), - protokoll.size()); + protokoll.size(), + LandesRechtTextAusgeber.ausgeben(gesetz), + abgleich); } /** Die Befehle eines Dokuments, geschieden nach dem, was am Stichtag schon galt. */ diff --git a/src/main/java/eu/mulk/aendggner/anwendung/Nachfassungsabgleich.java b/src/main/java/eu/mulk/aendggner/anwendung/Nachfassungsabgleich.java new file mode 100644 index 0000000..883d312 --- /dev/null +++ b/src/main/java/eu/mulk/aendggner/anwendung/Nachfassungsabgleich.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2026 Matthias Andreas Benkard <code@mail.matthias.benkard.de> +// SPDX-License-Identifier: AGPL-3.0-or-later +package eu.mulk.aendggner.anwendung; + +import eu.mulk.aendggner.gesetz.Gesetz; +import eu.mulk.aendggner.gesetz.Norm; +import java.util.ArrayList; +import java.util.List; + +/** + * Stellt die errechnete Fassung normweise gegen die amtliche Nachfassung. + * + * <p>Das ist der Maßstab, an dem jeder Belegfall dieses Erzeugnisses hängt: Nicht daran, dass + * sämtliche Befehle angewandt wurden, entscheidet sich seine Richtigkeit, sondern daran, dass der + * Wortlaut hinterher derselbe ist, den das Gesetzblatt und das Landesportal führen. Bis zu dieser + * Welle stand die Prüfung allein im Testcode; wer ein neues Land erschloss, musste sie ein zweites + * Mal schreiben. + * + * <p>Verglichen wird nach Normalisierung des Leerraums. Wie ein Portal umbricht, ist kein + * Rechtsinhalt — wohl aber jedes Wort und jedes Satzzeichen. + */ +public record Nachfassungsabgleich( + List<String> fehlende, + List<String> ueberzaehlige, + List<Abweichung> abweichungen, + int gleich, + int geprueft) { + + /** + * Eine Norm, die es beiderseits gibt und deren Wortlaut auseinanderfällt. + * + * @param soll der Wortlaut der amtlichen Nachfassung, normalisiert. + * @param ist der errechnete Wortlaut, normalisiert. + */ + public record Abweichung(String enbez, String soll, String ist) {} + + public Nachfassungsabgleich { + fehlende = List.copyOf(fehlende); + ueberzaehlige = List.copyOf(ueberzaehlige); + abweichungen = List.copyOf(abweichungen); + } + + /** + * @param soll die amtliche Nachfassung. + * @param ist die vom Erzeugnis fortgeschriebene Fassung. + */ + public static Nachfassungsabgleich vergleiche(Gesetz soll, Gesetz ist) { + var fehlende = new ArrayList<String>(); + var abweichungen = new ArrayList<Abweichung>(); + int gleich = 0; + + for (var normSoll : soll.normen()) { + var normIst = ist.norm(normSoll.enbez()); + if (normIst.isEmpty()) { + fehlende.add(normSoll.enbez()); + continue; + } + var wortlautSoll = wortlaut(normSoll); + var wortlautIst = wortlaut(normIst.orElseThrow()); + if (wortlautSoll.equals(wortlautIst)) { + gleich++; + } else { + abweichungen.add(new Abweichung(normSoll.enbez(), wortlautSoll, wortlautIst)); + } + } + + var ueberzaehlige = + ist.normen().stream().map(Norm::enbez).filter(enbez -> soll.norm(enbez).isEmpty()).toList(); + + return new Nachfassungsabgleich( + fehlende, ueberzaehlige, abweichungen, gleich, soll.normen().size()); + } + + /** Geht der Abgleich auf, so ist die errechnete Fassung die amtliche. */ + public boolean gehtAuf() { + return fehlende.isEmpty() && ueberzaehlige.isEmpty() && abweichungen.isEmpty(); + } + + /** „171 von 171 Normen gleich“ — die Zahl, die jeder Belegfall führt. */ + public String kurzbericht() { + var sb = new StringBuilder("%d von %d Normen gleich".formatted(gleich, geprueft)); + if (!fehlende.isEmpty()) { + sb.append("; %d fehlen".formatted(fehlende.size())); + } + if (!ueberzaehlige.isEmpty()) { + sb.append("; %d überzählig".formatted(ueberzaehlige.size())); + } + return sb.toString(); + } + + /** + * Die Überschrift gehört zum Wortlaut: Ein Befehl, der sie neu fasst, ändert die Norm ebenso wie + * einer, der ihren Text ändert, und eine Prüfung, die sie überginge, ließe gerade die Befehle + * ungeprüft, die auf Überschriften zielen. + */ + private static String wortlaut(Norm norm) { + var titel = norm.titel() == null ? "" : norm.titel() + "\n"; + return (titel + norm.gesamtText()).replaceAll("\\s+", " ").strip(); + } +} diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java index 1c87bc2..bef8a2c 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java @@ -125,6 +125,21 @@ final class LandesRechtTextParser { + "((?:\\p{Lu}|\\().*[^.]))?\\s*$"); /** + * Der Kopf einer aufgehobenen Sammelnorm: Zwei oder mehr weggefallene Paragraphen führt das + * Bundesrecht unter einer gemeinsamen Bezeichnung („§§ 17 u. 18“, Titel „(weggefallen)“); das + * gii-XML stellt ihr die Platzhalterkennung „(XXXX)“ voran, die zur Bezeichnung gehört und nicht + * zum Text. + * + * <p>Ohne diese Zeile könnte der kanonische Klartext kein Bundesgesetz tragen: Die Kopfzeile + * fiele in den Wortlaut der Vornorm und verfälschte ihn. Erkannt wird nur die aufgehobene Form — + * eine Sammelbezeichnung tragen ausschließlich weggefallene Normen, und die zwei Leerzeichen vor + * dem Titel sind dieselbe kanonische Marke wie beim gewöhnlichen Normkopf. + */ + private static final Pattern SAMMEL_NORM_KOPF = + Pattern.compile( + "^((?:\\(X+\\) )?(?:§§|Artt\\.) \\S.*?) {2}(\\((?:weggefallen|aufgehoben)\\))$"); + + /** * Der Kopf einer Nummer innerhalb einer Anlage („Nummer 6“). Die Nummern eines * Zuständigkeitskatalogs sind keine Aufzählungsglieder, sondern eigene Einheiten mit eigener * Absatzzählung — das Landesrechtsportal führt jede als eigene Einzelnorm, und die @@ -204,6 +219,7 @@ final class LandesRechtTextParser { var roemisch = zeile != null ? GLIEDERUNG_ROEMISCH.matcher(zeile) : null; var unterGliederung = zeile != null ? UNTER_GLIEDERUNG.matcher(zeile) : null; var normKopf = zeile != null ? NORM_KOPF.matcher(zeile) : null; + var sammelKopf = zeile != null ? SAMMEL_NORM_KOPF.matcher(zeile) : null; var anlagenKopf = zeile != null ? ANLAGEN_KOPF.matcher(zeile) : null; var uebersicht = INHALTSUEBERSICHT.equals(zeile); // Die Inhaltsübersicht führt die Gliederungs-Überschriften des Gesetzes als eigene Zeilen mit @@ -225,6 +241,7 @@ final class LandesRechtTextParser { && !imAnlagenteil && normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer); + boolean neueSammelNorm = zeile != null && !imAnlagenteil && sammelKopf.matches(); boolean neueAnlage = zeile != null && anlagenKopf.matches(); var anlagenNummer = zeile != null && imAnlagenteil ? ANLAGEN_NUMMER_KOPF.matcher(zeile) : null; @@ -234,6 +251,7 @@ final class LandesRechtTextParser { || uebersicht || gliederungsZeile || neueNorm + || neueSammelNorm || neueAnlage || neueAnlagenNummer) { // Laufende Norm abschließen. @@ -298,6 +316,11 @@ final class LandesRechtTextParser { normEnbez = normKopf.group(1) + " " + normKopf.group(2); normTitel = normKopf.group(3) != null ? normKopf.group(3).strip() : null; letzteNormNummer = numerisch(normKopf.group(2)); + } else if (neueSammelNorm) { + // Die Monotonieprobe bleibt unberührt: Eine Sammelbezeichnung trägt keine einzelne + // Nummer, an der sich fortzählen ließe. + normEnbez = sammelKopf.group(1); + normTitel = sammelKopf.group(2); } else if (roemisch.matches()) { gliederungsZaehler++; elternKennzahl = String.format("%03d", gliederungsZaehler); @@ -336,6 +359,7 @@ final class LandesRechtTextParser { || GLIEDERUNG_ARABISCH.matcher(zeile).matches() || GLIEDERUNG_ORDINALWORT.matcher(zeile).matches() || NORM_KOPF.matcher(zeile).matches() + || SAMMEL_NORM_KOPF.matcher(zeile).matches() || (roemisch.matches() && istUnterGliederung(roemisch, zeilen, i)); } diff --git a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java index 89cfd31..07449b8 100644 --- a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java +++ b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java @@ -55,6 +55,8 @@ public final class HtmlRenderer { sb.append("</div>\n"); + rendereAbgleich(sb, synopse); + rendereManuellZuPruefen(sb, synopse); rendereFuss(sb); @@ -232,6 +234,61 @@ public final class HtmlRenderer { }; } + /** + * Der Abgleich mit der amtlichen Nachfassung. Er steht vor dem Abschnitt „Manuell prüfen“, denn + * er beantwortet die vorrangige Frage: Nicht, ob jeder Befehl angewandt wurde, entscheidet über + * die Richtigkeit, sondern ob der Wortlaut hinterher derselbe ist. Die Abweichungen werden + * wortweise gezeigt — links, was amtlich steht, rechts, was errechnet wurde. + */ + private static void rendereAbgleich(StringBuilder sb, Synopse synopse) { + var abgleich = synopse.abgleich(); + if (abgleich == null) { + return; + } + sb.append("<section class=\"abgleich\">\n<h2>Abgleich mit der amtlichen Nachfassung</h2>\n"); + sb.append("<p class=\"bilanz") + .append(abgleich.gehtAuf() ? " geht-auf" : "") + .append("\">") + .append(esc(abgleich.kurzbericht())) + .append("</p>\n"); + liste(sb, "Im Erzeugnis fehlende Normen", abgleich.fehlende()); + liste(sb, "Im Erzeugnis überzählige Normen", abgleich.ueberzaehlige()); + if (!abgleich.abweichungen().isEmpty()) { + sb.append("<h3>Abweichender Wortlaut <span class=\"anzahl\">") + .append(abgleich.abweichungen().size()) + .append("</span></h3>\n<div class=\"gegenueberstellung\">\n") + .append("<div class=\"spaltenkopf\"><div>Amtliche Nachfassung</div>") + .append("<div>Errechnete Fassung</div></div>\n"); + for (var abweichung : abgleich.abweichungen()) { + var spalten = WortDiff.vergleiche(abweichung.soll(), abweichung.ist()); + sb.append("<section class=\"norm geaendert\">\n<h2>") + .append(esc(abweichung.enbez())) + .append("</h2>\n<div class=\"vergleich\">\n<div class=\"alt\">") + .append(spalten.altHtml()) + .append("</div>\n<div class=\"neu\">") + .append(spalten.neuHtml()) + .append("</div>\n</div>\n</section>\n"); + } + sb.append("</div>\n"); + } + sb.append("</section>\n"); + } + + private static void liste(StringBuilder sb, String ueberschrift, List<String> posten) { + if (posten.isEmpty()) { + return; + } + sb.append("<h3>") + .append(esc(ueberschrift)) + .append(" <span class=\"anzahl\">") + .append(posten.size()) + .append("</span></h3>\n<ul>\n"); + for (var posten1 : posten) { + sb.append("<li>").append(esc(posten1)).append("</li>\n"); + } + sb.append("</ul>\n"); + } + private static void rendereManuellZuPruefen(StringBuilder sb, Synopse synopse) { if (synopse.manuellZuPruefen().isEmpty() && synopse.warnungen().isEmpty() @@ -528,7 +585,7 @@ public final class HtmlRenderer { /* Die Abschnittsüberschriften sind Beschriftungsstreifen über die volle Breite, wie im Muster die Zeile „Verfügung des Finanzamts“. */ - section.gliederung-aenderungen > h2, section.manuell > h2 { + section.gliederung-aenderungen > h2, section.manuell > h2, section.abgleich > h2 { margin: 0 0 0.6rem; padding: 0.25rem 0.5rem; border: 1px solid var(--linie); @@ -558,6 +615,30 @@ public final class HtmlRenderer { text-align: center; } section.manuell li { margin-bottom: 0.6rem; font-size: 0.75rem; } + + /* Der Abgleich mit der amtlichen Nachfassung. Seine Bilanz ist die Zahl, auf die es + ankommt; sie steht deshalb im Kasten und nicht im Fließtext. Geht sie auf, so wird das + nicht durch Farbe gefeiert — ein Formblatt jubelt nicht —, sondern durch Fettung. */ + section.abgleich { margin-top: 1.5rem; padding-top: 1.25rem; } + section.abgleich h3 { margin: 1rem 0 0.4rem; font-size: 0.6875rem; } + section.abgleich h3 .anzahl { + display: inline-block; + min-width: 1.5rem; + margin-left: 0.35rem; + padding: 0 0.25rem; + border: 1px solid var(--linie); + background: var(--grund); + text-align: center; + } + section.abgleich li { font-size: 0.75rem; } + .bilanz { + margin: 0 0 0.6rem; + padding: 0.25rem 0.5rem; + border: 1px solid var(--linie); + font-size: 0.75rem; + text-align: center; + } + .bilanz.geht-auf { font-weight: bold; } .originaltext { font-family: var(--antiqua); color: var(--muted); font-size: 0.8125rem; } /* Vordrucknummer links, Ausgabestand rechts, beide winzig und ohne Zierrat. */ @@ -624,7 +705,7 @@ public final class HtmlRenderer { Stelle hatte nie eine — er sagt, was gedruckt und was eingetragen ist. */ del, ins, .badge, .entwurfshinweis, .spaltenkopf div, .vorspann dt, section.norm::before, - section.gliederung-aenderungen > h2, section.manuell > h2 { + section.gliederung-aenderungen > h2, section.manuell > h2, section.abgleich > h2 { -webkit-print-color-adjust: exact; print-color-adjust: exact; } diff --git a/src/main/java/eu/mulk/aendggner/synopse/Synopse.java b/src/main/java/eu/mulk/aendggner/synopse/Synopse.java index a2355cc..0c68176 100644 --- a/src/main/java/eu/mulk/aendggner/synopse/Synopse.java +++ b/src/main/java/eu/mulk/aendggner/synopse/Synopse.java @@ -4,6 +4,7 @@ package eu.mulk.aendggner.synopse; import eu.mulk.aendggner.aenderung.Inkrafttreten; import eu.mulk.aendggner.anwendung.BefehlAnwender.AngewandteAenderung; +import eu.mulk.aendggner.anwendung.Nachfassungsabgleich; import eu.mulk.aendggner.gesetz.Gesetz; import eu.mulk.aendggner.gesetz.Gliederung; import eu.mulk.aendggner.gesetz.Norm; @@ -19,6 +20,8 @@ import org.jspecify.annotations.Nullable; * @param stichtag der Tag, dessen Fassung gezeigt wird; {@code null} heißt: alle Befehle, ohne * Rücksicht auf das Inkrafttreten. * @param nichtInKraft die Befehle, die am Stichtag noch nicht galten und deshalb unterblieben. + * @param abgleich der normweise Vergleich mit einer amtlichen Nachfassung, soweit eine angegeben + * wurde; {@code null} sonst. */ public record Synopse( Gesetz alt, @@ -29,7 +32,8 @@ public record Synopse( List<String> warnungen, @Nullable Inkrafttreten inkrafttreten, @Nullable LocalDate stichtag, - List<AngewandteAenderung> nichtInKraft) { + List<AngewandteAenderung> nichtInKraft, + @Nullable Nachfassungsabgleich abgleich) { public Synopse( Gesetz alt, @@ -47,7 +51,26 @@ public record Synopse( warnungen, null, null, - List.of()); + List.of(), + null); + } + + /** + * Die Synopse mit dem Abgleich gegen eine amtliche Nachfassung. Er tritt erst nach dem Aufbau + * hinzu, weil er die fertige neue Fassung voraussetzt. + */ + public Synopse mitAbgleich(@Nullable Nachfassungsabgleich neuerAbgleich) { + return new Synopse( + alt, + neu, + eintraege, + gliederungsAenderungen, + manuellZuPruefen, + warnungen, + inkrafttreten, + stichtag, + nichtInKraft, + neuerAbgleich); } /** Eine geänderte Gliederungs-Überschrift (Teil/Abschnitt/…); {@code alt == null} bei neuen. */ diff --git a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java index 943c20a..2bc6359 100644 --- a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java +++ b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java @@ -80,7 +80,8 @@ public final class SynopseBuilder { parseWarnungen, inkrafttreten, stichtag, - nichtInKraft); + nichtInKraft, + null); } /** diff --git a/src/main/resources/eu/mulk/aendggner/web/app.js b/src/main/resources/eu/mulk/aendggner/web/app.js index fb55cbd..b72db3c 100644 --- a/src/main/resources/eu/mulk/aendggner/web/app.js +++ b/src/main/resources/eu/mulk/aendggner/web/app.js @@ -32,6 +32,11 @@ async function alsBase64(datei) { /** Hängt der Meldung einen Verweis auf das Ergebnis an; ein Blob, kein Server. */ function oeffne(inhalt, art, aufschrift) { + // Es können mehrere Ergebnisse nebeneinander stehen (Synopse und fortgeschriebene Fassung); + // dann trennt sie ein Punkt, damit die Aufschriften nicht aneinanderkleben. + if (meldung.querySelector("a")) { + meldung.append(" · "); + } const verweis = document.createElement("a"); verweis.href = URL.createObjectURL(new Blob([inhalt], { type: art })); verweis.target = "_blank"; @@ -49,6 +54,8 @@ formular.addEventListener("submit", async (ereignis) => { return; } + const nachfassungDatei = document.querySelector("#nachfassung").files[0] ?? null; + const neufassungGewuenscht = document.querySelector("#neufassung").checked; const artikel = document.querySelector("#artikel").value.trim(); const stichtag = document.querySelector("#stichtag").value.trim(); const nurText = document.querySelector("#nurtext").checked; @@ -58,9 +65,10 @@ formular.addEventListener("submit", async (ereignis) => { zeige("Lade das Rechenwerk (einmalig einige Megabyte) und werte aus …", "arbeit"); try { - const [stamm, patches] = await Promise.all([ + const [stamm, patches, nachfassung] = await Promise.all([ alsBase64(stammDatei), Promise.all(patchDateien.map(alsBase64)), + nachfassungDatei ? alsBase64(nachfassungDatei) : null, ]); if (!worker) { @@ -76,6 +84,7 @@ formular.addEventListener("submit", async (ereignis) => { vollstaendig, artikel: artikel === "" ? null : artikel, stichtag: stichtag === "" ? null : stichtag, + nachfassung, nurText, }); }); @@ -102,10 +111,18 @@ formular.addEventListener("submit", async (ereignis) => { // aufgenommen, so sind das nicht die geänderten Normen, und sie heißen dann anders. zeige( `${ergebnis.angewandt} Befehle angewandt, ${ergebnis.manuell} manuell zu prüfen, ` + - `${ergebnis.normen} ${vollstaendig ? "Normen in der Synopse" : "geänderte Normen"}. `, + `${ergebnis.normen} ${vollstaendig ? "Normen in der Synopse" : "geänderte Normen"}. ` + + (ergebnis.abgleich ? `Abgleich: ${ergebnis.abgleich}. ` : ""), "fertig", ); oeffne(ergebnis.html, "text/html;charset=utf-8", "Synopse öffnen"); + if (neufassungGewuenscht) { + oeffne( + ergebnis.neufassung, + "text/plain;charset=utf-8", + "Fortgeschriebene Fassung öffnen", + ); + } } catch (e) { zeige("Verarbeitung fehlgeschlagen: " + (e && e.message ? e.message : e), "fehler"); } finally { diff --git a/src/main/resources/eu/mulk/aendggner/web/index.html b/src/main/resources/eu/mulk/aendggner/web/index.html index 69adbdb..40d93bb 100644 --- a/src/main/resources/eu/mulk/aendggner/web/index.html +++ b/src/main/resources/eu/mulk/aendggner/web/index.html @@ -160,6 +160,24 @@ ausgewiesen. Ohne Angabe werden alle Befehle angewandt)</span></label> <input type="date" id="stichtag" name="stichtag" autocomplete="off"> </div> + <div class="feld"> + <label for="nachfassung">Amtliche Nachfassung + <span class="erl">(nur auszufüllen, wenn das Ergebnis dagegen zu halten ist: Die + Synopse weist alsdann Norm für Norm aus, worin die errechnete Fassung von der + amtlichen abweicht. Angenommen werden dieselben Formate wie beim + Stammgesetz)</span></label> + <input type="file" id="nachfassung" name="nachfassung" + accept=".xml,.zip,.pdf,.txt,text/plain,application/pdf"> + </div> + <div class="feld ankreuzfeld"> + <label> + <input type="checkbox" id="neufassung" name="neufassung"> + Auch die fortgeschriebene Fassung als Klartext ausgeben + </label> + <p class="erl nachsatz">Sie lässt sich sichern und als Stammgesetz eines weiteren + Änderungsheftes wieder einreichen — so entsteht die Fortschreibung über mehrere + Hefte hinweg.</p> + </div> <div class="feld ankreuzfeld"> <label> <input type="checkbox" id="nurtext" name="nurtext"> diff --git a/src/main/resources/eu/mulk/aendggner/web/worker.js b/src/main/resources/eu/mulk/aendggner/web/worker.js index 259b629..f38d6a7 100644 --- a/src/main/resources/eu/mulk/aendggner/web/worker.js +++ b/src/main/resources/eu/mulk/aendggner/web/worker.js @@ -19,7 +19,8 @@ const gestartet = GraalVM.run([], Object.assign(new GraalVM.Config(), { })); self.onmessage = async (nachricht) => { - const { stamm, patches, vollstaendig, artikel, stichtag, nurText } = nachricht.data; + const { stamm, patches, vollstaendig, artikel, stichtag, nachfassung, nurText } = + nachricht.data; try { await gestartet; await bereit; @@ -29,6 +30,7 @@ self.onmessage = async (nachricht) => { artikel: artikel ?? null, stichtag: stichtag ?? null, vollstaendig: Boolean(vollstaendig), + nachfassung: nachfassung ?? null, nurText: Boolean(nurText), }); if (ergebnis.fehler) { @@ -41,6 +43,8 @@ self.onmessage = async (nachricht) => { angewandt: Number(ergebnis.angewandt), manuell: Number(ergebnis.manuell), normen: Number(ergebnis.normen), + neufassung: String(ergebnis.neufassung), + abgleich: ergebnis.abgleich ? String(ergebnis.abgleich) : null, }); } } catch (e) { |
