aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java53
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java3
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java5
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java21
4 files changed, 79 insertions, 3 deletions
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 6fb511d..d9ecf8a 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
@@ -73,6 +73,17 @@ public final class TextBereiniger {
private static final Pattern KONJUNKTION =
Pattern.compile("^(und|oder|sowie|bzw\\.|beziehungsweise)\\b.*");
+ /** Perzentil der Zeilenlängen, das als „volle Spaltenbreite“ gilt (siehe {@link #verbindeUmbrueche}). */
+ private static final double VOLLZEILE_PERZENTIL = 0.9;
+
+ /** Mindestanteil der vollen Spaltenbreite, ab dem ein markerloser Umbruch als Silbentrennung
+ * statt als bewusster Wortgrenzen-Umbruch gilt. */
+ private static final double VOLLZEILE_MINDESTANTEIL = 0.7;
+
+ /** Anzahl Zeilen vor/nach einer Kandidatenzeile, die für die lokale Spaltenbreiten-Schätzung
+ * herangezogen werden (siehe {@link #typischeZeilenlaenge}). */
+ private static final int VOLLZEILE_FENSTER = 20;
+
// BMJV-Entwurfsvorlagen zeichnen das hängende öffnende Anführungszeichen im Content-Stream
// NACH dem ersten Element der zitierten Passage: „(1) „ Ungeachtet…“ statt „„(1) Ungeachtet…“,
// „§ 19„“ statt „„§ 19“.
@@ -183,7 +194,11 @@ public final class TextBereiniger {
* <li><b>Markerlos</b> (Bundestags-Drucksachen: „Schwel“ + „lenwertes“): Reguläre Umbrüche
* enden dort mit Leerzeichen vor dem Zeilenumbruch; endet eine Zeile direkt mit einem
* Buchstaben und beginnt die Folgezeile klein, ist es eine Trennung → ohne Leerzeichen
- * zusammenziehen.
+ * zusammenziehen. Das trifft aber nur zu, wenn die Zeile (fast) die volle Spaltenbreite
+ * ausnutzt — sonst wäre der Umbruch dort nicht nötig gewesen. Kurze, bewusst
+ * abgebrochene Zeilen (z.B. ein Stichwort vor einer hängend eingerückten Definition:
+ * „…Nachhaltigkeitssiegels“ + „das Anbringen …“) werden deshalb ausgenommen — sie sind
+ * ein Wortgrenzen-Umbruch, keine Silbentrennung, auch wenn das Trailing-Space-Signal fehlt.
* </ul>
*/
private static ArrayList<String> verbindeUmbrueche(List<String> zeilen) {
@@ -198,7 +213,10 @@ public final class TextBereiniger {
while (true) {
var gestutzt = zeile.stripTrailing();
var mitTrennstrich = endetMitSilbentrennung(gestutzt);
- var markerlos = markerlosAktiv && endetMarkerlos(zeile);
+ var markerlos =
+ markerlosAktiv
+ && endetMarkerlos(zeile)
+ && gestutzt.length() >= typischeZeilenlaenge(zeilen, i) * VOLLZEILE_MINDESTANTEIL;
if (!mitTrennstrich && !markerlos) {
break;
}
@@ -266,6 +284,37 @@ public final class TextBereiniger {
return mitTrailingSpace > 0 && mitTrailingSpace * 4 >= nichtLeer;
}
+ /**
+ * Typische „volle“ Zeilenlänge im Umfeld von {@code zentrum} ({@link #VOLLZEILE_PERZENTIL}-
+ * Perzentil der gestutzten Längen nichtleerer Zeilen in einem Fenster von {@link
+ * #VOLLZEILE_FENSTER} Zeilen davor/danach) — ein grober Näherungswert für die lokale
+ * Spaltenbreite, ohne auf PDF-Positionsdaten zugreifen zu müssen. Lokal statt dokumentweit, weil
+ * ein einziges Dokument Abschnitte mit unterschiedlicher Spaltenbreite mischen kann (z.B.
+ * schmalerer Regelungstext vs. breitere Begründung in Regierungsentwürfen) — eine dokumentweite
+ * Kennzahl würde dort die kürzere Spalte systematisch benachteiligen. Vereinzelte überlange
+ * Zeilen (z.B. selbst fälschlich verklebte Umbrüche) dürfen den Wert nicht verzerren, daher ein
+ * hohes Perzentil statt des reinen Maximums.
+ */
+ private static int typischeZeilenlaenge(List<String> zeilen, int zentrum) {
+ var laengen = new ArrayList<Integer>();
+ int von = Math.max(0, zentrum - VOLLZEILE_FENSTER);
+ int bis = Math.min(zeilen.size(), zentrum + VOLLZEILE_FENSTER + 1);
+ for (int i = von; i < bis; i++) {
+ var zeile = zeilen.get(i);
+ if (zeile.isBlank()) {
+ continue;
+ }
+ laengen.add(zeile.stripTrailing().length());
+ }
+ if (laengen.isEmpty()) {
+ return 0;
+ }
+ laengen.sort(null);
+ int index = (int) (laengen.size() * VOLLZEILE_PERZENTIL);
+ index = Math.min(index, laengen.size() - 1);
+ return laengen.get(index);
+ }
+
private static String strippeZeilenenden(List<String> zeilen) {
var sb = new StringBuilder();
for (var zeile : zeilen) {
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 4140357..68fe2b8 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -460,7 +460,8 @@ public final class BefehlAnwender {
}
// Neufassung eines Satzes / einer Nummer / eines Buchstabens: Bereich ersetzen.
- return bearbeiteText(normen, befehl, text -> TextErgebnis.ok(befehl.neuerText().strip()));
+ return bearbeiteText(
+ normen, befehl, text -> TextErgebnis.ok(normalisiereZitatText(befehl.neuerText())));
}
/** Ein Ziel (Absatz, Satz, Nummer, Buchstabe) wird durch einen Block ersetzt (ggf. 1 → N). */
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index d4b0b17..a395fa1 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -84,6 +84,11 @@ class EndToEndTest {
assertThat(text).doesNotContain("Dieses Gesetz dient der Umsetzung der Richtlinie (EU)");
assertThat(text).contains("wird wie folgt geändert");
+ // Regression: Stichwort/Definition-Umbruch im Anhang darf nicht ohne Leerzeichen verklebt
+ // werden (kurze Stichwort-Zeile vor hängend eingerückter Definition, kein Wort wird getrennt).
+ assertThat(text).contains("Nachhaltigkeitssiegels\ndas Anbringen");
+ assertThat(text).doesNotContain("Nachhaltigkeitssiegelsdas");
+
var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
assertThat(parseErgebnis.artikel()).containsExactly("1");
assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(10);
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 98dc1d3..4ca86cf 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
@@ -100,6 +100,27 @@ class TextBereinigerTest {
}
@Test
+ void ziehtMarkerlosNichtBeiKurzemStichwortVorEingerueckterDefinitionZusammen() {
+ // Definitionslisten-Muster (neues BGBl-Format, „2a. …Siegel“ + hängend eingerückte
+ // Definition): die Stichwort-Zeile endet ohne Trailing-Space, ist aber deutlich kürzer als
+ // die umgebenden Volltextzeilen — kein Wort wird getrennt, der Umbruch ist bewusst.
+ var roh =
+ "1. „Betriebsstoff“ jeder Bestandteil einer Ware, der wiederholt verbraucht wird und ersetzt"
+ + " oder aufgefüllt werden muss, damit die Ware ordnungsgemäß funktioniert. \n"
+ + "2. „Haltbarkeit“ die Fähigkeit der Waren, ihre erforderlichen Funktionen und ihre"
+ + " Leistung bei normaler Verwendung über einen längeren Zeitraum zu bewahren. \n"
+ + "3. „Zertifizierungssystem“ ein System der Überprüfung durch Dritte, durch das"
+ + " bestätigt wird, dass ein Produkt bestimmten Anforderungen entspricht. \n"
+ + "2a. unzulässiges Anbringen eines Nachhaltigkeitssiegels\n"
+ + "das Anbringen eines Nachhaltigkeitssiegels, das weder auf einem Zertifizierungssystem"
+ + " beruht noch von \n"
+ + "staatlichen Stellen festgesetzt wurde;";
+
+ assertThat(TextBereiniger.bereinige(roh))
+ .contains("Nachhaltigkeitssiegels\ndas Anbringen eines Nachhaltigkeitssiegels");
+ }
+
+ @Test
void repariertInvertierteZitatzeichenAnAbsatzmarkern() {
// BMJV-Vorlagen zeichnen das hängende „ nach dem Absatzmarker bzw. der Paragraphenangabe.
assertThat(TextBereiniger.bereinige("(1) „ Ungeachtet des § 8 gilt.“"))