aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/eu/mulk/aendggner/AendGgner.java55
-rw-r--r--src/main/java/eu/mulk/aendggner/Pipeline.java137
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/Nachfassungsabgleich.java100
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java24
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java85
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/Synopse.java27
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java3
-rw-r--r--src/main/resources/eu/mulk/aendggner/web/app.js21
-rw-r--r--src/main/resources/eu/mulk/aendggner/web/index.html18
-rw-r--r--src/main/resources/eu/mulk/aendggner/web/worker.js6
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) {