diff options
Diffstat (limited to 'src/main/java')
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/AendGgner.java | 35 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java | 12 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/gesetz/Superskript.java | 15 | ||||
| -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); } } |
