From 81777013dfa582053489efea874e84379f1f0805 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 26 Jul 2026 08:53:10 +0200 Subject: Support unnumbered named Anlagen; strip control characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigating "Anlagen as amendment target" corrected the diagnosis from yesterday: Anlagen are supported. In gii-XML they are norms of their own ("Anlage 8", "Anhang"), Stelle.anlagenEnbez() resolves them, and the GEG sample applies annex-targeted commands — only two of its fifty manual entries touch annexes, both for content reasons. What actually failed in Berlin was narrower, and is fixed: * A law with a single annex names it after the provision it belongs to ("Die Anlage zu § 2 Absatz 4 Satz 1"). StellenParser demanded a number after "Anlage" and rejected the phrase, so the article's frame command went unrecognised and its items lost their context. The annex now carries the designation "Anlage", like "Anhang". The naming suffix is skipped deliberately: read along, the "§ 2" inside it would become the target and the wrong norm would be amended. * C0 control characters are now stripped. Berlin's enumeration indent carries a U+0007 that sits invisibly in front of the command and made its start-of-line anchor miss. * Command verbs torn apart by a stray space in the official typesetting ("ein gefügt" for "eingefügt") are rejoined, restricted to sequences that are not valid German word order. Three of Article 1's four commands are now recognised. The fourth places the new unit by a word anchor ("Vor den Wörtern … wird folgender Absatz 5 eingefügt"), which StrukturEinfuegung cannot express; that and the inability of the plain-text stem format to carry an Anlage at all are recorded in Landesrecht-Beispiele.adoc. 231 tests green; all pinned figures unchanged. Co-Authored-By: Claude Opus 5 Change-Id: Ic5804bf343dd5d8ce435c07b8f882ca0372890d2 --- .../mulk/aendggner/aenderung/parse/StellenParser.java | 19 ++++++++++++++++++- .../aendggner/aenderung/parse/TextBereiniger.java | 13 +++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) (limited to 'src/main/java') diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java index 473eb14..786c812 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java @@ -133,7 +133,7 @@ public final class StellenParser { i++; } case "Teil", "Teils", "Buch", "Buches", "Kapitel", "Kapitels", "Abschnitt", "Abschnitts", - "Unterabschnitt", "Unterabschnitts", "Anlage", "Anlagen" -> { + "Unterabschnitt", "Unterabschnitts" -> { var wert = naechstesWort(woerter, i); if (wert == null || !NUMMER_WERT.matcher(wert).matches()) { return Optional.empty(); @@ -141,6 +141,23 @@ public final class StellenParser { komponenten.add(new Stelle.Gliederungseinheit(gliederungsArt(wort), wert)); i++; } + case "Anlage", "Anlagen" -> { + var wert = naechstesWort(woerter, i); + if (wert != null && NUMMER_WERT.matcher(wert).matches()) { + komponenten.add(new Stelle.Gliederungseinheit("Anlage", wert)); + i++; + continue; + } + // „die Anlage zu § 2 Absatz 4 Satz 1“ — ein Gesetz mit einer einzigen Anlage benennt sie + // nach der Vorschrift, zu der sie gehört. Wie beim „Anhang“ trägt sie dann die enbez + // „Anlage“; der Zusatz identifiziert sie nur und ist nicht selbst Änderungsziel. Er + // reicht bis zum Ende der Stellenangabe und wird deshalb übersprungen — sonst läse der + // Parser das darin genannte „§ 2“ als Ziel und änderte die falsche Norm. + komponenten.add(new Stelle.Gliederungseinheit("Anlage", "")); + if ("zu".equals(wert)) { + i = woerter.length; + } + } case "Anhang" -> komponenten.add(new Stelle.Gliederungseinheit("Anhang", "")); case "Inhaltsübersicht" -> komponenten.add(new Stelle.Inhaltsuebersicht()); case "Überschrift" -> komponenten.add(new Stelle.Ueberschrift()); 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 d0e37f8..fc54554 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -206,12 +206,19 @@ public final class TextBereiniger { private static final Pattern SACHNUMMER_MIT_LEERZEICHEN = Pattern.compile("(§|Art\\.) (\\d+) ([a-z])(?![a-zäöüß).])"); + /** C0-Steuerzeichen außer Tabulator und Zeilenumbruch; im Fließtext stets Extraktionsmüll. */ + private static final Pattern STEUERZEICHEN = Pattern.compile("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]"); + private TextBereiniger() {} public static String bereinige(String rohText) { // Geschützte Leerzeichen (GVBl-Satz: „§  1“, „Abs.  2“) sind für Javas \s und // String.strip unsichtbar — früh auf gewöhnliche Leerzeichen normalisieren. var text = rohText.replace(' ', ' ').replace(' ', ' '); + // Steuerzeichen aus fehlgeleiteten Glyphenzuordnungen (im GVBl. für Berlin trägt der Einzug + // der Aufzählungsglieder ein U+0007). Sie sind unsichtbar, stehen aber vor dem Befehlstext und + // ließen dessen Zeilenanfangs-Anker ins Leere greifen. + text = STEUERZEICHEN.matcher(text).replaceAll(""); text = normalisiereAnfuehrungszeichen(text); text = INVERTIERTES_ZITAT.matcher(text).replaceAll("$1„($2) "); text = INVERTIERTES_PARAGRAPH_ZITAT.matcher(text).replaceAll("$1„$2"); @@ -290,6 +297,12 @@ public final class TextBereiniger { .replace("undwird ", "und wird ") .replace("Kommaeingefügt", "Komma eingefügt") .replace("Kommaersetzt", "Komma ersetzt") + // Umgekehrter Satzfehler: ein Leerzeichen mitten in der Befehlsvokabel („ein ge-“ + + // „fügt“ → „ein gefügt“, GVBl. Berlin 2026/17). Nur Fügungen, die als Wortfolge im + // Deutschen nicht vorkommen — „er setzt“ etwa bliebe unangetastet. + .replace("ein gefügt", "eingefügt") + .replace("an gefügt", "angefügt") + .replace("auf gehoben", "aufgehoben") // Kontextrahmen, an den der folgende Unterpunkt geklebt wurde („geändertaa) In …“). .replaceAll("(wie folgt geändert:?)(?=[a-z]{1,3}\\)|\\d+[a-z]?\\.)", "$1\n"); } -- cgit v1.2.1