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.java35
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java12
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/Superskript.java15
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java (renamed from src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtLoader.java)26
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java (renamed from src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParser.java)65
5 files changed, 102 insertions, 51 deletions
diff --git a/src/main/java/eu/mulk/aendggner/AendGgner.java b/src/main/java/eu/mulk/aendggner/AendGgner.java
index e647b75..efa95a9 100644
--- a/src/main/java/eu/mulk/aendggner/AendGgner.java
+++ b/src/main/java/eu/mulk/aendggner/AendGgner.java
@@ -6,8 +6,9 @@ import eu.mulk.aendggner.aenderung.parse.SuperskriptModus;
import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
import eu.mulk.aendggner.anwendung.BefehlAnwender;
import eu.mulk.aendggner.gesetz.Gesetz;
-import eu.mulk.aendggner.gesetz.bayern.BayRechtLoader;
+import eu.mulk.aendggner.gesetz.Superskript;
import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader;
+import eu.mulk.aendggner.gesetz.land.LandesRechtLoader;
import eu.mulk.aendggner.synopse.HtmlRenderer;
import eu.mulk.aendggner.synopse.SynopseBuilder;
import java.io.IOException;
@@ -125,7 +126,8 @@ public class AendGgner implements Callable<Integer> {
}
if (extractOnly) {
- var extraktor = new PatchTextExtraktor(superskriptModus());
+ var gesetz = ladeStammgesetz();
+ var extraktor = new PatchTextExtraktor(superskriptModus(gesetz));
for (var file : patches) {
var text = extraktor.extrahiere(file);
System.out.println(raw ? text : TextBereiniger.bereinige(text));
@@ -135,7 +137,7 @@ public class AendGgner implements Callable<Integer> {
if (dumpBefehle) {
var gesetz = ladeStammgesetz();
- var extraktor = new PatchTextExtraktor(superskriptModus());
+ var extraktor = new PatchTextExtraktor(superskriptModus(gesetz));
var parser = new AenderungsgesetzParser();
for (var file : patches) {
var text = TextBereiniger.bereinige(extraktor.extrahiere(file));
@@ -177,7 +179,7 @@ public class AendGgner implements Callable<Integer> {
// Pipeline: Stammgesetz laden → Befehle parsen → anwenden → Synopse rendern.
var altesGesetz = ladeStammgesetz();
- var extraktor = new PatchTextExtraktor(superskriptModus());
+ var extraktor = new PatchTextExtraktor(superskriptModus(altesGesetz));
var parser = new AenderungsgesetzParser();
var gesetz = altesGesetz;
@@ -222,16 +224,27 @@ public class AendGgner implements Callable<Integer> {
return gesamtErgebnis.anzahlAngewandt() == 0 && !protokoll.isEmpty() ? 2 : 0;
}
- /** Gii-XML → {@link GiiXmlLoader}; PDF/Klartext (bayerisches Landesrecht) → {@link
- * BayRechtLoader}. */
+ /** Gii-XML → {@link GiiXmlLoader}; PDF/Klartext (Landesrecht) → {@link LandesRechtLoader}. */
private Gesetz ladeStammgesetz() throws Exception {
- return istGiiXml() ? new GiiXmlLoader().load(baseFile) : new BayRechtLoader().load(baseFile);
+ return istGiiXml() ? new GiiXmlLoader().load(baseFile) : new LandesRechtLoader().load(baseFile);
}
- /** Bayerische Stammtexte tragen amtliche Satznummern — auch die Änderungsgesetze werden dann
- * mit Superskript-Erhalt extrahiert, damit Zitate und Stammtext dieselbe Schreibweise tragen. */
- private SuperskriptModus superskriptModus() throws IOException {
- return istGiiXml() ? SuperskriptModus.ENTFERNEN : SuperskriptModus.BEHALTEN;
+ /**
+ * Der Superskriptmodus folgt der Schreibweise des Stammgesetzes: Trägt es amtliche Satznummern
+ * (bayerisches Landesrecht, Niedersachsen u.a.), werden auch die Änderungsgesetze mit
+ * Superskript-Erhalt extrahiert, damit Zitate und Stammtext dieselbe Schreibweise tragen; sonst
+ * (Bundesrecht, Länder ohne amtliche Satzzählung) sind hochgestellte Ziffern bloße
+ * Fußnotenmarker und werden verworfen.
+ */
+ private SuperskriptModus superskriptModus(Gesetz gesetz) {
+ for (var norm : gesetz.normen()) {
+ for (var absatz : norm.absaetze()) {
+ if (Superskript.traegtSatznummern(absatz.text())) {
+ return SuperskriptModus.BEHALTEN;
+ }
+ }
+ }
+ return SuperskriptModus.ENTFERNEN;
}
private boolean istGiiXml() throws IOException {
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
index edbe742..fb578cc 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -50,8 +50,14 @@ final class BefehlErkenner {
// Aufzählungslabel, das in Entwürfen/Drucksachen vor dem Zitat steht („… gefasst: 3. „…““).
private static final String ENUM = "((?:\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s*)?";
+ // Verb der Neufassung. Neben „wird/werden wie folgt gefasst“ (Bund/Bayern) auch „erhält/erhalten
+ // folgende Fassung“ — die in mehreren Ländern (Schleswig-Holstein, Niedersachsen) übliche Form.
+ // Rein zusätzliche Alternation ohne eigene Fanggruppe, damit die Gruppennummern gleich bleiben.
+ private static final String NEUFASSUNG_VERB =
+ "(?:(?:wird|werden) wie folgt gefasst|(?:erhält|erhalten) folgende Fassung)";
+
private static final Pattern NEUFASSUNG =
- Pattern.compile("^(.+?) (?:wird|werden) wie folgt gefasst: " + ENUM + Z + "\\.?$");
+ Pattern.compile("^(.+?) " + NEUFASSUNG_VERB + ": " + ENUM + Z + "\\.?$");
// „§ 2 Absatz 2 wird durch die folgenden Absätze 2 und 3 ersetzt: „…““ (neues BGBl-Format);
// auch „Die Überschrift wird durch die folgende Überschrift ersetzt: „…““ (Entwürfe).
@@ -340,9 +346,7 @@ final class BefehlErkenner {
// der Zitatblock wird an „§ N“-Grenzen in Einzel-Neufassungen zerlegt.
private static final Pattern PARAGRAPH_BEREICH_NEUFASSUNG =
Pattern.compile(
- "^Die (?:§§|Artt?\\.) (\\d+[a-z]?) bis (\\d+[a-z]?) (?:wird|werden) wie folgt gefasst: "
- + Z
- + "\\.?$");
+ "^Die (?:§§|Artt?\\.) (\\d+[a-z]?) bis (\\d+[a-z]?) " + NEUFASSUNG_VERB + ": " + Z + "\\.?$");
// „Der Wortlaut wird Absatz 1.“, bayerisch auch „Der bisherige Wortlaut wird Abs. 5.“ und
// „Der Wortlaut wird Satz 1.“
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java b/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java
index 777fe35..733c1f7 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java
@@ -46,6 +46,21 @@ public final class Superskript {
}
/**
+ * Wahr, wenn {@code text} mindestens eine amtliche Satznummer am Satzanfang trägt. Dient dazu, die
+ * Schreibweise eines Stammgesetzes zu erkennen (Landesrecht mit amtlicher Satzzählung behält seine
+ * Superskripte, Bundesrecht ohne solche verwirft sie).
+ */
+ public static boolean traegtSatznummern(String text) {
+ var m = LAUF.matcher(text);
+ while (m.find()) {
+ if (istSatzanfang(text, m.start(), m.end())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Wahr, wenn der Superskript-Lauf {@code [start, ende)} in {@code text} eine amtliche Satznummer
* am Satzanfang ist — im Unterschied zum Fußnotenmarker, der einem Wort anhängt und auf den eine
* schließende Klammer folgt („Enteignung⁶)“).
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtLoader.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java
index 4150af4..6bd72ce 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtLoader.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java
@@ -1,4 +1,4 @@
-package eu.mulk.aendggner.gesetz.bayern;
+package eu.mulk.aendggner.gesetz.land;
import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor;
import eu.mulk.aendggner.aenderung.parse.SuperskriptModus;
@@ -12,19 +12,21 @@ import org.apache.tika.Tika;
import org.jboss.logging.Logger;
/**
- * Liest ein Stammgesetz des bayerischen Landesrechts aus der konsolidierten Fassung von
- * gesetze-bayern.de — als PDF oder als kanonischer Klartext.
+ * Liest ein Stammgesetz des Landesrechts aus der konsolidierten Fassung — als PDF oder als
+ * kanonischer Klartext. Deckt bayerisches Landesrecht (Gliederung in {@code Art.}, amtliche
+ * Satznummern) ebenso ab wie die {@code §}-gegliederten Gesetze der übrigen Länder; das Sigel
+ * folgt je Norm aus dem Text (siehe {@link LandesRechtTextParser}).
*
* <p>PDFs durchlaufen dieselbe Aufbereitung wie Änderungsgesetze (Fontgrößen-Filter mit
* Superskript-Erhalt, Textbereinigung); der bereinigte Lineartext ist zugleich das dokumentierte
- * Klartext-Format (siehe {@link BayRechtTextParser}): Wo die PDF-Extraktion versagt oder nur eine
- * andere Quelle verfügbar ist (etwa eine archivierte HTML-Fassung), kann der Text von Hand
- * aufbereitet und als {@code .txt} eingespeist werden. Amtliche Satznummern und Fußnotenmarker
- * stehen dabei als Unicode-Superskripte im Text (¹Die freilebende Tierwelt …, Enteignung⁶)).
+ * Klartext-Format: Wo die PDF-Extraktion versagt oder nur eine andere Quelle verfügbar ist (etwa
+ * eine archivierte HTML-Fassung), kann der Text von Hand aufbereitet und als {@code .txt}
+ * eingespeist werden. Amtliche Satznummern und Fußnotenmarker stehen dabei als Unicode-Superskripte
+ * im Text (¹Die freilebende Tierwelt …, Enteignung⁶)).
*/
-public final class BayRechtLoader {
+public final class LandesRechtLoader {
- private static final Logger log = Logger.getLogger(BayRechtLoader.class);
+ private static final Logger log = Logger.getLogger(LandesRechtLoader.class);
private final Tika tika = new Tika();
@@ -44,15 +46,15 @@ public final class BayRechtLoader {
"Nicht unterstützter Dateityp %s für Stammgesetz %s (unterstützt: PDF, Klartext)"
.formatted(mimeType, datei));
};
- return BayRechtTextParser.parse(text);
+ return LandesRechtTextParser.parse(text);
}
/**
* Stellt einen vom Zeilen-Reflow an das Satzende der Vornorm geklebten Normkopf („… verlangen.
* Art. 17 Jagderlaubnis“) wieder auf eine eigene Zeile. Das doppelte Leerzeichen zwischen
- * Artikelnummer und Titel unterscheidet den Normkopf von gewöhnlichen Querverweisen.
+ * Norm-Nummer und Titel unterscheidet den Normkopf von gewöhnlichen Querverweisen.
*/
private static String nachSatzendeGetrennteNormkoepfe(String text) {
- return text.replaceAll("(?<=[.“?!])[ \\t]+(?=Art\\.\\s\\d+[a-z]?[ \\t]{2})", "\n");
+ return text.replaceAll("(?<=[.“?!])[ \\t]+(?=(?:§|Art\\.)\\s\\d+[a-z]?[ \\t]{2})", "\n");
}
}
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
index 0744086..b1da42d 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParser.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
@@ -1,4 +1,4 @@
-package eu.mulk.aendggner.gesetz.bayern;
+package eu.mulk.aendggner.gesetz.land;
import eu.mulk.aendggner.gesetz.Absatz;
import eu.mulk.aendggner.gesetz.Gesetz;
@@ -11,9 +11,12 @@ import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
/**
- * Parst den kanonischen Lineartext eines bayerischen Stammgesetzes (gesetze-bayern.de) zu einem
- * {@link Gesetz}. Das Layout entspricht der {@code --extract-only}-Ausgabe der konsolidierten
- * PDF-Fassung (siehe {@link BayRechtLoader}):
+ * Parst den kanonischen Lineartext eines Landesrecht-Stammgesetzes zu einem {@link Gesetz}. Das
+ * Layout entspricht der {@code --extract-only}-Ausgabe der konsolidierten PDF-Fassung (siehe {@link
+ * LandesRechtLoader}). Die Norm-Gliederung folgt dem jeweiligen Land: der Bund und die meisten
+ * Länder zitieren in {@code §}, Bayern in {@code Art.} — das Sigel wird je Norm aus dem Normkopf
+ * abgeleitet und nach {@link Norm#enbez()} durchgereicht, ein zentrales „Land“-Merkmal ist nicht
+ * nötig.
*
* <pre>
* Bayerisches Jagdgesetz ← Langtitel
@@ -23,17 +26,20 @@ import org.jspecify.annotations.Nullable;
* BayRS 792-1-W
* Vollzitat nach RedR: … ← übersprungen (ggf. mehrzeilig)
* I. Abschnitt Grundsätze ← Gliederungs-Überschrift
- * 1. Allgemeine Vorschriften ← nummerierte Unter-Überschrift (vor einem Art.-Kopf)
- * Art. 1 Gesetzeszweck ← Normkopf; „Art. 60 (aufgehoben)“ → weggefallen
+ * 1. Allgemeine Vorschriften ← nummerierte Unter-Überschrift (vor einem Normkopf)
+ * Art. 1 Gesetzeszweck ← Normkopf (§/Art.); „Art. 60 (aufgehoben)“ → weggefallen
* (1) ¹Die freilebende Tierwelt … ← Absätze mit amtlichen Satznummern als Superskript
* ⁶) [Amtl. Anm.:] … ← Fußnotenzeile, verbleibt im Text des tragenden Absatzes
* </pre>
*/
-final class BayRechtTextParser {
+final class LandesRechtTextParser {
private static final Pattern ABSATZ_MARKER = Pattern.compile("^\\((\\d+[a-z]?)\\)\\s+");
- private static final Pattern JURABK_ZEILE = Pattern.compile("^\\((\\S+)\\)$");
+ // Juris-Abkürzung direkt hinter dem Langtitel. Bayern führt einteilige Kürzel („(BayJG)“), die
+ // übrigen Länder oft mehrteilige („(GO NRW)“); Nur die Zeile unmittelbar nach dem Titel wird
+ // geprüft, sodass spätere Fundstellen-Klammern („(BayRS V S. 595)“) nicht getroffen werden.
+ private static final Pattern JURABK_ZEILE = Pattern.compile("^\\(([^()]+)\\)$");
// Kopf der Druckfassung („BayJG: Bayerisches Jagdgesetz … (Art. 1–64)“), ggf. mit
// umbrochenem Rest („1–64)“) auf der Folgezeile.
@@ -45,13 +51,17 @@ final class BayRechtTextParser {
private static final Pattern UNTER_GLIEDERUNG = Pattern.compile("^(\\d+[a-z]?)\\.\\s+(\\S.*)$");
- // Normkopf: „Art. N“ plus Titel auf derselben Zeile. Die Negativliste schließt
- // Querverweise am Zeilenanfang aus („Art. 4 Abs. 3 …“).
+ // Normkopf: „§ N“ bzw. „Art. N“ plus Titel auf derselben Zeile. Das Sigel steht in Gruppe 1, die
+ // Nummer in Gruppe 2, der Titel in Gruppe 3. Die Negativliste schließt Querverweise am
+ // Zeilenanfang aus („Art. 4 Abs. 3 …“, „§ 5 Absatz 2 …“).
private static final Pattern NORM_KOPF =
Pattern.compile(
- "^Art\\.\\s+(\\d+[a-z]?)\\s+"
- + "(?!Absatz|Abs\\.|Satz|Sätze|Nummer|Nr\\.|Buchstabe|Buchst\\."
- + "|und|bis|oder|sowie|des|der|dieses)"
+ "^(§|Art\\.)\\s+(\\d+[a-z]?)\\s+"
+ // Querverweis-Schlüsselwörter nur als ganzes Wort ausschließen: „§ 4 Satz 2“ ist ein
+ // Verweis, „§ 4 Satzungen“ dagegen ein Normtitel. Der Schutz „(?![a-zäöüß])“ verhindert,
+ // dass „Satz“ auch „Satzungen“, „Nummer“ auch „Nummerierung“ trifft.
+ + "(?!(?:Absatz|Abs\\.|Satz|Sätze|Nummer|Nr\\.|Buchstabe|Buchst\\."
+ + "|und|bis|oder|sowie|des|der|dieses)(?![a-zäöüß]))"
+ "((?:\\p{Lu}|\\().*)$");
private static final Pattern WEGGEFALLEN_TITEL =
@@ -60,7 +70,7 @@ final class BayRechtTextParser {
private static final Pattern FUSSNOTE =
Pattern.compile("^([⁰¹²³⁴⁵⁶⁷⁸⁹]+|\\d{1,3})\\)\\s*(\\[Amtl\\. Anm\\.:\\].*)$");
- private BayRechtTextParser() {}
+ private LandesRechtTextParser() {}
static Gesetz parse(String text) {
var zeilen = text.lines().toList();
@@ -95,6 +105,7 @@ final class BayRechtTextParser {
String elternKennzahl = null;
int gliederungsZaehler = 0;
+ String normSigel = null;
String normNummer = null;
String normTitel = null;
var normZeilen = new ArrayList<String>();
@@ -109,11 +120,11 @@ final class BayRechtTextParser {
if (zeile == null
|| gliederung.matches()
- || (normKopf.matches() && istNeuerNormKopf(normKopf.group(1), letzteNormNummer))
+ || (normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer))
|| (unterGliederung.matches() && istUnterGliederung(unterGliederung, zeilen, i))) {
// Laufende Norm abschließen.
if (normNummer != null) {
- normen.add(baueNorm(normNummer, normTitel, aktuelleGliederung, normZeilen));
+ normen.add(baueNorm(normSigel, normNummer, normTitel, aktuelleGliederung, normZeilen));
}
normNummer = null;
normZeilen.clear();
@@ -131,9 +142,10 @@ final class BayRechtTextParser {
gliederung.group(1) + ". " + gliederung.group(2),
titel.isEmpty() ? null : titel);
gliederungen.add(aktuelleGliederung);
- } else if (normKopf.matches() && istNeuerNormKopf(normKopf.group(1), letzteNormNummer)) {
- normNummer = normKopf.group(1);
- normTitel = normKopf.group(2).strip();
+ } else if (normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer)) {
+ normSigel = normKopf.group(1);
+ normNummer = normKopf.group(2);
+ normTitel = normKopf.group(3).strip();
letzteNormNummer = numerisch(normNummer);
} else {
var kennzahl =
@@ -152,8 +164,8 @@ final class BayRechtTextParser {
if (normen.isEmpty()) {
throw new IllegalArgumentException(
- "Kein „Art. N“-Normkopf gefunden — ist das eine konsolidierte Fassung von"
- + " gesetze-bayern.de?");
+ "Kein „§ N“- oder „Art. N“-Normkopf gefunden — ist das eine konsolidierte Fassung im"
+ + " kanonischen Klartextformat?");
}
return new Gesetz(jurabk != null ? jurabk : langue, langue, null, normen, gliederungen);
}
@@ -211,8 +223,12 @@ final class BayRechtTextParser {
}
private static Norm baueNorm(
- String nummer, @Nullable String titel, @Nullable Gliederung gliederung, List<String> zeilen) {
- var enbez = "Art. " + nummer;
+ String sigel,
+ String nummer,
+ @Nullable String titel,
+ @Nullable Gliederung gliederung,
+ List<String> zeilen) {
+ var enbez = sigel + " " + nummer;
boolean weggefallen = titel != null && WEGGEFALLEN_TITEL.matcher(titel).matches();
var absaetze = new ArrayList<Absatz>();
@@ -245,6 +261,7 @@ final class BayRechtTextParser {
absaetze.add(new Absatz(absatzNummer, String.join("\n", absatzZeilen)));
}
- return new Norm(enbez, titel == null || titel.isEmpty() ? null : titel, gliederung, absaetze, weggefallen);
+ return new Norm(
+ enbez, titel == null || titel.isEmpty() ? null : titel, gliederung, absaetze, weggefallen);
}
}