diff options
| author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-07-25 09:11:56 +0200 |
|---|---|---|
| committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-07-25 09:11:56 +0200 |
| commit | 519c1e7b769c37769d1b5d1ffd6e13566b26c86d (patch) | |
| tree | 59c8deaa1e34c3464801e9337fc2a92a15af307d | |
| parent | 43e91612875de3cc6e7466eee56023ab55cefd89 (diff) | |
Add Schleswig-Holstein extraction case and three cleanup fixes
The GVOBl. Schl.-H. amendment act (Kommunalrecht, 2026/27) brings three
conventions the tool had not seen:
* a running two-line page header that sits between articles and glued
itself onto the following article heading,
* superscript footnote markers on article headings ("Artikel 1 ¹)"),
which defeated the article split, and
* ragged-right typesetting, where the geometric margin detection finds
no alignment cluster and reports full lines as hard line ends, so a
trailing hyphen was never joined.
All three are fixed in TextBereiniger; the hyphen rule now trusts the
extractor's trailing-space signal over the geometric class.
A full acceptance test is not possible: the Schleswig-Holstein law portal
serves document bodies only through an authenticated API, and the last
freely archived consolidated Gemeindeordnung (14 Nov 2022) predates the
amended § 34a. The new test therefore covers everything up to and
including command recognition; the blocker is recorded in
Landesrecht-Beispiele.adoc.
219 tests green; Bund/Bayern/Sachsen/Niedersachsen figures unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: I9b3ab8a756372504b51c4d410b491fded255da96
| -rw-r--r-- | FASSUNGEN.txt | 66 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java | 43 | ||||
| -rw-r--r-- | src/main/resources/sampledata/Landesrecht-Beispiele.adoc | 14 | ||||
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/EndToEndTest.java | 54 | ||||
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java | 34 |
5 files changed, 207 insertions, 4 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt index d3605b2..e09cc8a 100644 --- a/FASSUNGEN.txt +++ b/FASSUNGEN.txt @@ -21,6 +21,72 @@ Vorbemerkung zur Führung dieses Verzeichnisses ════════════════════════════════════════════════════════════════════════════════ + Fassung vom 25. Juli 2026, + zuletzt geändert durch die am 25. Juli 2026 vorgenommenen Änderungen +════════════════════════════════════════════════════════════════════════════════ + +Die sechste Ertüchtigungswelle wird um den schleswig-holsteinischen Belegfall +ergänzt: das Gesetz zur Änderung des Gesetzes zur Änderung kommunalrechtlicher +Vorschriften vom 26. März 2026 (GVOBl. Schl.-H. 2026/27). Es führt drei bislang +unbekannte Eigenheiten ein — den laufenden Seitenkopf des GVOBl. Schleswig- +Holstein, hochgestellte Fußnotenmarker an den Artikel-Überschriften und einen +Flattersatz, in dem die geometrische Randerkennung versagt. Ein voller +Akzeptanztest bleibt vorerst aus, weil die konsolidierte Gemeindeordnung aus +freien Quellen nicht in der maßgeblichen Fassung zu beschaffen ist. + + +Artikel 1 +Seitenkopf des Gesetz- und Verordnungsblatts für Schleswig-Holstein + +Der Textbereiniger (TextBereiniger.istKolumnentitel) entfernt nunmehr auch den +zweizeiligen Titelkopf und den laufenden Seitenkopf des Gesetz- und Verordnungs- +blatts für Schleswig-Holstein („Gesetz- und Verordnungsblatt für Schleswig- +Holstein“ nebst „2026/27 vom 30. März“, Muster GVOBL_SH_KOPF, GVOBL_SH_LAND und +GVOBL_SH_HEFT). Der laufende Kopf steht zwischen zwei Artikeln und klebte sonst +an der folgenden Artikel-Überschrift. + + +Artikel 2 +Fußnotenmarker an Artikel-Überschriften + +Schleswig-Holstein verweist an der Artikel-Überschrift eines Änderungsgesetzes +mit einer hochgestellten Ziffer auf die Gliederungsnummer-Fußnote am Seitenfuß +(„Artikel 1 ¹)“). Solche Überschriften genügten dem Muster der Artikel-Teilung +(AenderungsgesetzParser.ARTIKEL_UEBERSCHRIFT) nicht. Der Textbereiniger streift +den Marker nunmehr ab (TextBereiniger.loeseArtikelFussnoten, Muster +ARTIKEL_FUSSNOTE) und trennt dabei eine in derselben Zeile stehende Einleitung +wieder ab. + + +Artikel 3 +Silbentrennung bei hartem Zeilenende + +Der Textbereiniger (TextBereiniger.verbindeUmbrueche) zog eine Silbentrennung am +Zeilenende bislang nie zusammen, wenn die geometrische Randerkennung des +Fontgrößenfilters ein hartes Zeilenende meldete. Im Flattersatz — wie ihn das +GVOBl. Schl.-H. verwendet — findet diese Erkennung jedoch keinen Ausrichtungs- +Cluster und stuft auch volle Zeilen als hart ein. Klebt der Trennstrich ohne +Trailing-Whitespace am Zeilenende, ist er deshalb nunmehr auch dort als +Silbentrennung zu lesen; bewusst gesetzte Suspensivstriche tragen den Trailing- +Whitespace des Extraktors und bleiben unberührt. + + +Artikel 4 +Belegfall Kommunalrecht-Änderungsgesetz Schleswig-Holstein + +Als weiterer Prüffall tritt ein Test bis einschließlich der Befehlserkennung +hinzu (EndToEndTest.kommunalrechtAendGSchleswigHolstein): Artikel 1 trifft die +Gemeindeordnung, Artikel 2 die Kreisordnung, Artikel 3 ein anderes Änderungs- +gesetz; je Zielgesetz wird genau ein Neufassungsbefehl („§ 34a Absatz 1 erhält +folgende Fassung“, „§ 29a Absatz 1 …“) erkannt, kein Befehl bleibt unbekannt. +Ein voller Akzeptanztest ist nicht möglich: Das Landesportal +gesetze-rechtsprechung.sh.juris.de gibt seine Dokumentinhalte nur über eine +anmeldepflichtige Schnittstelle aus, und die letzte frei archivierte Gesamt- +ausgabe der Gemeindeordnung (14. November 2022) kennt den geänderten § 34a noch +nicht. Die Fundstelle ist in sampledata/Landesrecht-Beispiele.adoc vermerkt. + + +════════════════════════════════════════════════════════════════════════════════ Fassung vom 24. Juli 2026, zuletzt geändert durch die am 24. Juli 2026 vorgenommenen Änderungen ════════════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java index 9c1edaa..7460b15 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -1,5 +1,6 @@ package eu.mulk.aendggner.aenderung.parse; +import eu.mulk.aendggner.gesetz.Superskript; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; @@ -131,6 +132,19 @@ public final class TextBereiniger { private static final Pattern NDS_HERAUSGEBER = Pattern.compile("^\\s*Herausgeber: Niedersächsische Staatskanzlei\\s*$"); + // GVOBl. Schleswig-Holstein: zweizeiliger Titelkopf („Gesetz- und Verordnungsblatt“ / „für + // Schleswig-Holstein“, auf Folgeseiten in einer Zeile) und die Heftangabe („Nummer 2026/27“ auf + // dem Titelblatt, „2026/27 vom 30. März“ als zweite Zeile des laufenden Seitenkopfs). Der + // laufende Kopf steht mitten im Text zwischen zwei Artikeln und klebte sonst an der folgenden + // Artikel-Überschrift. + private static final Pattern GVOBL_SH_KOPF = + Pattern.compile("^\\s*Gesetz- und Verordnungsblatt(?: für Schleswig-Holstein)?\\s*$"); + private static final Pattern GVOBL_SH_LAND = + Pattern.compile("^\\s*für Schleswig-Holstein\\s*$"); + private static final Pattern GVOBL_SH_HEFT = + Pattern.compile( + "^\\s*(?:Nummer \\d{4}/\\d{1,3}|\\d{4}/\\d{1,3} vom \\d{1,2}\\. \\p{L}+)\\s*$"); + /** Konjunktionen, die typischerweise auf einen Suspensivstrich folgen („Wirk- und …“). */ private static final Pattern KONJUNKTION = Pattern.compile("^(und|oder|sowie|bzw\\.|beziehungsweise)\\b.*"); @@ -192,7 +206,7 @@ public final class TextBereiniger { var verbunden = verbindeUmbrueche(zeilen); // Falsch-positive markerlose Zusammenzüge („durch“ + „die“ → „durchdie“) reparieren — die // Befehlsvokabeln sind nie Kompositum-Bestandteile. - return trenneVerklebteZitatgrenzen(reflowUndStrippe(verbunden)); + return loeseArtikelFussnoten(trenneVerklebteZitatgrenzen(reflowUndStrippe(verbunden))); } /** @@ -262,6 +276,20 @@ public final class TextBereiniger { .replaceAll("(wie folgt geändert:?)(?=[a-z]{1,3}\\)|\\d+[a-z]?\\.)", "$1\n"); } + /** + * Streift hochgestellte Fußnotenmarker von Artikel-Überschriften ab („Artikel 1 ¹)“ → „Artikel + * 1“). Schleswig-Holstein verweist damit auf die Gl.-Nr.-Fußnoten am Seitenfuß. Der Marker + * verhindert sonst die Artikel-Teilung; steht die Einleitung in derselben Zeile, wird sie dabei + * wieder abgetrennt. + */ + private static String loeseArtikelFussnoten(String text) { + return ARTIKEL_FUSSNOTE.matcher(text).replaceAll("$1\n"); + } + + private static final Pattern ARTIKEL_FUSSNOTE = + Pattern.compile( + "(?m)^(Artikel\\s+\\d+[a-z]?)\\s*" + Superskript.LAUF.pattern() + "\\)[ \\t]*\\n?"); + /** Entfernt Seitenkopf-/Fußzeilen. Trailing-Whitespace der übrigen Zeilen bleibt erhalten! */ private static ArrayList<Zeile> entferneKolumnentitel(List<Zeile> roh) { var kolumnentitel = new boolean[roh.size()]; @@ -302,7 +330,10 @@ public final class TextBereiniger { || LANDTAG_MARKE.matcher(zeile).matches() || REVOSAX_FUSS.matcher(zeile).matches() || NDS_GVBL_FUSS.matcher(zeile).matches() - || NDS_HERAUSGEBER.matcher(zeile).matches(); + || NDS_HERAUSGEBER.matcher(zeile).matches() + || GVOBL_SH_KOPF.matcher(zeile).matches() + || GVOBL_SH_LAND.matcher(zeile).matches() + || GVOBL_SH_HEFT.matcher(zeile).matches(); } /** @@ -342,7 +373,13 @@ public final class TextBereiniger { while (true) { var gestutzt = zeile.stripTrailing(); // Ein geometrisch hartes Zeilenende ist ein bewusster Umbruch — nie zusammenziehen. - var mitTrennstrich = umbruch != Umbruch.HART && endetMitSilbentrennung(gestutzt); + // Ausnahme: Klebt der Trennstrich ohne Trailing-Whitespace am Zeilenende, ist er eine + // echte Silbentrennung. Im Flattersatz (GVOBl. Schl.-H.) findet die Randerkennung keinen + // Ausrichtungs-Cluster und stuft auch volle Zeilen als hart ein; das Trailing-Space-Signal + // des Extraktors ist dort das verlässlichere Kriterium. + var mitTrennstrich = + endetMitSilbentrennung(gestutzt) + && (umbruch != Umbruch.HART || gestutzt.length() == zeile.length()); var markerlos = markerlosAktiv && endetMarkerlos(zeile) diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc index f358ae4..373f615 100644 --- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc +++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc @@ -88,12 +88,24 @@ folgt geändert"). dem zweispaltigen Nds. GVBl 2022 Nr. 33) und `EndToEndTest.nefgAcceptance` (fünf von sieben Befehlen angewandt; zwei Einfügeformen — Sachnummer mit Leerzeichen „§ 2 a“, kapitelbezogene §-Block-Einfügung — noch nicht unterstützt, als Grenze dokumentiert). +* *Schleswig-Holstein* ist bis einschließlich der Befehlserkennung umgesetzt + (`EndToEndTest.kommunalrechtAendGSchleswigHolstein`): Artikel 1 trifft die Gemeindeordnung, + Artikel 2 die Kreisordnung, je ein Neufassungsbefehl, kein unbekannter Befehl. Ein voller + Akzeptanztest ist *nicht* möglich — siehe „Noch offen“. * Baden-Württemberg (GBl 2026 Nr. 26): großes Anlage-lastiges Änderungsgesetz (§§ 4–57a + Anlagen 1/3b/4b); Stammfassung/Akzeptanztest noch offen. -* Für Schleswig-Holstein fehlt die konsolidierte Stammfassung noch (siehe oben). == Noch offen +* *Schleswig-Holstein — Stammfassung nicht beschaffbar*: Das Landesportal + `gesetze-rechtsprechung.sh.juris.de` ist seit Ende 2022 eine reine Single-Page-Anwendung; die + Dokumentinhalte liegen hinter einer anmeldepflichtigen „mylex“-API (`connectUrl` aus dem + `localStorage`), sämtliche Dokument-URLs liefern nur die leere Anwendungshülle. Die Wayback + Machine hat die serverseitig gerenderte Gesamtausgabe der Gemeindeordnung zuletzt am + 14.11.2022 erfasst — dort fehlt der hier geänderte § 34a noch (er wurde erst durch das Gesetz + vom 05.02.2025 eingefügt). Auch die archivierten Einzelnorm-Seiten (113 Stück) sind bereits + SPA-Hüllen ohne Inhalt. Ein voller Akzeptanztest bleibt daher offen, bis die konsolidierte + Fassung aus einer anderen Quelle vorliegt. * *Rheinland-Pfalz*: Die Verkündungsplattform `verkuendung.rlp.de` verkündet erst seit dem 01.07.2026 elektronisch; ältere GVBl-Hefte liegen nicht als freie Direkt-PDF vor. * Bremen, Hamburg, Mecklenburg-Vorpommern, Saarland, Brandenburg, Sachsen-Anhalt: noch nicht diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 90ad4ed..d36341d 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -3,16 +3,19 @@ package eu.mulk.aendggner; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl; import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser; import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor; import eu.mulk.aendggner.aenderung.parse.TextBereiniger; import eu.mulk.aendggner.anwendung.BefehlAnwender; +import eu.mulk.aendggner.gesetz.Gesetz; import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader; import eu.mulk.aendggner.synopse.HtmlRenderer; import eu.mulk.aendggner.synopse.SynopseBuilder; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import org.junit.jupiter.api.Test; /** @@ -428,4 +431,55 @@ class EndToEndTest { var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false); assertThat(HtmlRenderer.rendere(synopse, "E2E-Test NEFG")).contains("§ 1"); } + + /** + * Schleswig-Holstein: Extraktion und Befehlserkennung auf dem Kommunalrecht-Änderungsgesetz + * (GVOBl. Schl.-H. 2026/27). Kein voller Akzeptanztest — die konsolidierte Gemeindeordnung ist + * aus freien Quellen nicht in der maßgeblichen Fassung zu beschaffen (das juris-Landesportal + * liefert nur eine anmeldepflichtige API; die letzten frei archivierten Volltexte stammen aus + * 2022 und kennen den hier geänderten § 34a noch nicht). Geprüft wird deshalb alles bis + * einschließlich der Befehlserkennung, gegen ein Stammgesetz ohne Normen. + */ + @Test + void kommunalrechtAendGSchleswigHolstein() throws Exception { + var pdf = SAMPLEDATA.resolve("SchleswigHolstein/GVOBl-2026-27_Kommunalrecht-AendG.pdf"); + assumeTrue(Files.exists(pdf), "GVOBl-Schl.-H.-Beispiel-PDF fehlt"); + + var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf)); + + // Laufender Seitenkopf entfernt — er stand zwischen Artikel 1 und Artikel 2 und klebte sonst + // an der Artikel-2-Überschrift. + assertThat(text).doesNotContain("Gesetz- und Verordnungsblatt für Schleswig-Holstein"); + assertThat(text).doesNotContain("2026/27 vom 30. März"); + // Hochgestellte Fußnotenmarker der Artikel-Überschriften („Artikel 1 ¹)“) sind abgestreift. + assertThat(text).contains("\nArtikel 1\n").contains("\nArtikel 2\n").contains("\nArtikel 3\n"); + assertThat(text).doesNotContain("¹)").doesNotContain("²)").doesNotContain("³)"); + // Silbentrennung im Flattersatz zusammengezogen (dort findet die geometrische Randerkennung + // keinen Ausrichtungs-Cluster und meldet auch volle Zeilen als hartes Zeilenende). + assertThat(text).contains("Ton-Bild-Übertragung zu ermöglichen").doesNotContain("Übertra-"); + + // Artikel 1 ändert die Gemeindeordnung, Artikel 2 die Kreisordnung, Artikel 3 ein anderes + // Änderungsgesetz — nur der jeweils zutreffende Artikel wird gewählt. + var gemeindeordnung = + new Gesetz("GO", "Gemeindeordnung für Schleswig-Holstein", "Gemeindeordnung", List.of()); + var kreisordnung = + new Gesetz("KrO", "Kreisordnung für Schleswig-Holstein", "Kreisordnung", List.of()); + + var go = new AenderungsgesetzParser().parse(text, gemeindeordnung, null); + assertThat(go.artikel()).containsExactly("1"); + assertThat(go.befehle()).hasSize(1); + assertThat(go.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl); + var neufassung = go.befehle().get(0); + assertThat(neufassung.stelle().anzeigeText()).isEqualTo("§ 34a Absatz 1"); + assertThat(neufassung).isInstanceOf(Aenderungsbefehl.Neufassung.class); + // Das Zitat der neuen Fassung ist vollständig übernommen („erhält folgende Fassung“). + assertThat(((Aenderungsbefehl.Neufassung) neufassung).neuerText()) + .startsWith("(1) Durch Hauptsatzung kann bestimmt werden") + .endsWith("persönlich im Sitzungsraum anwesend sein."); + + var kro = new AenderungsgesetzParser().parse(text, kreisordnung, null); + assertThat(kro.artikel()).containsExactly("2"); + assertThat(kro.befehle()).hasSize(1); + assertThat(kro.befehle().get(0).stelle().anzeigeText()).isEqualTo("§ 29a Absatz 1"); + } } diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java index 755db46..f49e41b 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java @@ -202,6 +202,40 @@ class TextBereinigerTest { } @Test + void ziehtSilbentrennungAuchBeiHartemZeilenendeZusammen() { + // Flattersatz (GVOBl. Schl.-H.): Die Randerkennung findet keinen Ausrichtungs-Cluster und + // meldet auch volle Zeilen als hartes Zeilenende. Klebt der Trennstrich ohne Trailing- + // Whitespace am Zeilenende, ist er dennoch eine Silbentrennung. + assertThat(TextBereiniger.bereinige("Teilnahme mittels Ton-Bild-Übertra-\ngung. ")) + .isEqualTo("Teilnahme mittels Ton-Bild-Übertragung."); + // Mit Trailing-Whitespace bleibt das harte Zeilenende ein bewusster Umbruch. + assertThat(TextBereiniger.bereinige("Wirk-, Ausgangs- \nund Hilfsstoffe ")) + .isEqualTo("Wirk-, Ausgangs-\nund Hilfsstoffe"); + } + + @Test + void streiftFussnotenmarkerVonArtikelUeberschriften() { + // Schleswig-Holstein verweist an der Artikel-Überschrift auf die Gl.-Nr.-Fußnote; steht die + // Einleitung in derselben Zeile, wird sie dabei wieder abgetrennt. + assertThat(TextBereiniger.bereinige("Artikel 1 ¹)\nDie Gemeindeordnung")) + .isEqualTo("Artikel 1\nDie Gemeindeordnung"); + assertThat(TextBereiniger.bereinige("Artikel 2 ²) Die Kreisordnung")) + .isEqualTo("Artikel 2\nDie Kreisordnung"); + } + + @Test + void entferntSeitenkopfDesGvoblSchleswigHolstein() { + var roh = + """ + Erster Satz. + Gesetz- und Verordnungsblatt für Schleswig-Holstein + 2026/27 vom 30. März + Zweiter Satz."""; + + assertThat(TextBereiniger.bereinige(roh)).isEqualTo("Erster Satz.\nZweiter Satz."); + } + + @Test void repariertInvertierteZitatzeichenAnAbsatzmarkern() { // BMJV-Vorlagen zeichnen das hängende „ nach dem Absatzmarker bzw. der Paragraphenangabe. assertThat(TextBereiniger.bereinige("(1) „ Ungeachtet des § 8 gilt.“")) |
