diff options
8 files changed, 243 insertions, 76 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt index d3195bb..9ea3b7e 100644 --- a/FASSUNGEN.txt +++ b/FASSUNGEN.txt @@ -32,7 +32,7 @@ UWG-Anhang) sowohl im PDF-Extraktor als auch im XML-Flattener vereinheitlicht. Artikel 1 -Geometrische Spaltenbreiten-Klassifikation für markerlose Zeilenumbrüche +Geometrische Klassifikation der Zeilenenden nach Ausrichtungs-Clustern Der Textbereiniger (TextBereiniger.verbindeUmbrueche) zog eine Zeile, die ohne Leerraum am Ende mit einem Buchstaben schließt und deren Folgezeile klein @@ -41,37 +41,61 @@ Annahme, es handle sich um eine bei der PDF-Extraktion um den Bindestrich gebrachte Silbentrennung. Diese Annahme trifft auf einen bewussten Wortgrenzen- Umbruch (etwa das Stichwort einer hängend eingerückten Definition, wie er im Anhang zum UWG vorkommt) nicht zu. Zur Behebung bestimmt der PDF-Vorverarbeiter -(FontgroessenFilter) das geometrische End-X der Zeilenläufe und klassifiziert -die Zeilenenden anhand des lokalen rechten Randes (90. Perzentil in einem -Fenster von 20 Zeilen davor/danach) in weiche (am Rand endende), harte (deutlich -davor endende) oder unklassifizierte Umbrüche. Der Textbereiniger zieht nur noch -geometrisch weiche oder (in Ermangelung von Geometriedaten als Rückfalloption) -zeichenzahlnäherungsweise am Rand liegende markerlose Umbrüche zusammen. Harte -Umbrüche bleiben als gewollte Wortgrenzen-Umbrüche erhalten. +(FontgroessenFilter) das geometrische End-X jeder Zeile (unter Flush auch am +Seitenende, damit die letzte Zeile einer Seite nicht die erste der Folgeseite +verfälscht) und klassifiziert die Zeilenenden anhand von Ausrichtungs-Clustern +in einem Fenster von 20 Zeilen davor/danach: Ein Zeilenende ist weich, wenn +mindestens fünf Fensterzeilen (auf vier Punkt genau) gleich ausgerichtet enden +(Satzspiegelrand des Blocksatzes), und hart, wenn ein solcher Rand mindestens +zehn Punkt oberhalb des Zeilenendes verläuft — dort wäre noch Platz gewesen, +der Umbruch ist mithin gewollt. Cluster statt eines Fenster-Perzentils, weil am +Spaltenwechsel des zweispaltigen alten Bundesgesetzblatts beide Spaltenränder +in ein Fenster fallen; maßgeblich ist der Rand, an dem die Zeile selbst +ausgerichtet ist. Der Textbereiniger zieht markerlose Umbrüche nur noch bei +weichen Zeilenenden zusammen (in Ermangelung von Geometriedaten näherungsweise +nach Zeichenzahl wie bisher), nie jedoch vor einer Folgezeile, die mit einem +Aufzählungsmarker („d)“, „3.“) beginnt. Artikel 2 +Zusammenzug weicher und Erhaltung harter Umbrüche + +Nach den Einzelzusammenzügen fügt der Textbereiniger geometrisch weiche +Zeilenenden mit einem Leerzeichen zu Fließtext zusammen (Reflow); harte und +unklassifizierte Umbrüche bleiben erhalten. Nach der Bereinigung ist damit +jeder verbleibende Zeilenumbruch nach bester Einschätzung beabsichtigt, weshalb +die Zitat-Normalisierung des Befehlsanwenders (normalisiereZitatText) Umbrüche +nicht mehr zu Leerzeichen faltet. Ausgenommen vom Reflow sind Strukturzeilen +(„Artikel 2“, „§ 19“, allein stehende Gliederungs-Bezeichnungen): Gleich +breite, zentrierte Überschriften in Serie bilden ein Schein-Cluster, der Parser +benötigt sie jedoch allein auf der Zeile. + + +Artikel 3 Strukturierung und Normalisierung hängend eingerückter Definitionslisten Damit die nunmehr erhaltenen hängenden Zeilenenden in Definitionslisten (wie -beim UWG-Anhang) korrekt strukturiert werden: -1. Der Befehlsanwender (BefehlAnwender.normalisiereZitatText) unterwirft nunmehr - auch die stellenlose Neufassung (z.B. eines Satzes) der Zitat-Normalisierung - und rückt fortlaufende Zeilen innerhalb eines Aufzählungspunktes tiefer - (vier Leerzeichen) ein als den Aufzählungspunkt selbst (zwei Leerzeichen). -2. Der XML-Flattener (ContentFlattener) trennt mehrere <LA>-Geschwister innerhalb - eines <DD>-Elements durch Zeilenumbrüche und rückt sie tiefer ein. -Beide Wege erzeugen dieselbe kanonische Zeilenform, welche für die korrekte -Erkennung von Kindzeilen durch die Stellenauflösung erforderlich ist. +beim UWG-Anhang oder in § 2 IfSG) korrekt strukturiert werden: +1. Der XML-Flattener (ContentFlattener) trennt mehrere <LA>-Geschwister innerhalb + eines <DD>-Elements durch Zeilenumbrüche und rückt die Folgezeilen tiefer ein + als die Aufzählungszeile (Kindzeilen der Einheit für die Stellenauflösung). +2. Der Befehlsanwender rückt beim Einsetzen von Zitattext (rueckeZitatEin) + Fortsetzungszeilen zwei Zeichen tiefer ein als die Aufzählungszeile. +Beide Wege erzeugen dieselbe kanonische Zeilenform („ 2a. Stichwort“ nebst +„ Definitionstext“), welche für die korrekte Erkennung von Kindzeilen durch +die Stellenauflösung (StellenAufloeser.zeilenBlock) erforderlich ist. Schlussbestimmung Die vorstehenden Änderungen sind durch die Prüfung sämtlicher Testfälle (einhun- -dertvierundsiebzig an der Zahl) sowie durch die vollständige Erstellung (mvnw -verify) bestätigt worden. Anlass war ein anhand der UWG-Novelle 2026 gemeldeter -Wiedergabefehler; die Behebung wurde anhand der Beispieldaten (UWG-Anhang, -Nummern 2a sowie 4a bis 4c) verifiziert. +dertneunundsiebzig an der Zahl), durch die vollständige Erstellung (mvnw verify) +sowie durch einen Vorher-nachher-Vergleich sämtlicher einundzwanzig Beispiel- +kombinationen (UWG, AGG, ProdHaftG, GEG, IfSG; Anwendungs- und Prüfzahlen +durchgehend unverändert) bestätigt worden. Anlass war ein anhand der UWG-Novelle +2026 gemeldeter Wiedergabefehler (verklebte Kurzüberschriften im Anhang, auch in +der Grundvorschrift); die Behebung wurde anhand der Beispieldaten (UWG-Anhang, +Nummern 2a, 4c sowie 22; § 2 IfSG) verifiziert. ════════════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/FontgroessenFilter.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/FontgroessenFilter.java index 45ce32a..964d856 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/FontgroessenFilter.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/FontgroessenFilter.java @@ -60,27 +60,25 @@ final class FontgroessenFilter { private static final java.util.regex.Pattern ENDX_REST = java.util.regex.Pattern.compile("\uE002\\d*\uE002?"); - /** Fensterhälfte (Zeilen davor/danach) für die lokale Schätzung des rechten Rands. Lokal statt - * dokumentweit, weil ein Dokument Blöcke unterschiedlicher Spaltenbreite mischt (schmalerer - * Regelungstext vs. breitere Begründung; zweispaltiges altes BGBl, dessen Spalten in - * Content-Stream-Reihenfolge nacheinander kommen). */ + /** Fensterhälfte (Zeilen davor/danach) für die lokale Suche nach Ausrichtungs-Clustern. Lokal + * statt dokumentweit, weil ein Dokument Blöcke unterschiedlicher Spaltenbreite mischt + * (schmalerer Regelungstext vs. breitere Begründung; zweispaltiges altes BGBl, dessen Spalten + * in Content-Stream-Reihenfolge nacheinander kommen). */ private static final int RAND_FENSTER = 20; - /** Perzentil der End-X-Werte im Fenster, das als rechter Rand gilt (robust gegen einzelne - * überlange Artefaktzeilen, anders als das Maximum). */ - private static final double RAND_PERZENTIL = 0.9; + /** Streuung (pt), innerhalb derer Zeilenenden als „gleich ausgerichtet“ gelten. Blocksatz-Zeilen + * enden auf wenige pt genau am Rand; ein evtl. mitgemessenes Trailing-Space verschiebt das Ende + * um eine Leerzeichenbreite (~2–3 pt). */ + private static final float CLUSTER_TOLERANZ_PT = 4f; - /** Bis zu diesem Abstand (pt) unter dem Rand endet eine Zeile „am Rand“ → weicher Umbruch. - * Blocksatz-Zeilen enden auf wenige pt genau am Rand. */ - private static final float WEICH_TOLERANZ_PT = 3f; - - /** Ab diesem Abstand (pt) unter dem Rand ist das Zeilenende bewusst gesetzt → harter Umbruch. - * Der Bereich dazwischen bleibt unklassifiziert (z.B. Zeilen, deren gefilterte + /** Ab diesem Abstand (pt) unter einem Ausrichtungs-Cluster ist ein Zeilenende bewusst gesetzt → + * harter Umbruch. Der Bereich dazwischen bleibt unklassifiziert (z.B. Zeilen, deren gefilterte * Fußnotenziffer das gemessene Ende leicht verkürzt). */ private static final float HART_ABSTAND_PT = 10f; - /** Mindestzahl von Fensterzeilen am Rand, damit das Fenster als Blocksatz gilt und überhaupt - * klassifiziert wird — Titelseiten, Inhaltsübersichten u.ä. bleiben unklassifiziert. */ + /** Mindestzahl gleich ausgerichteter Fensterzeilen, damit ein Zeilenende als Satzspiegelrand + * (Ausrichtungs-Cluster) gilt — Titelseiten, Unterschriftenblöcke u.ä. bilden keine Cluster + * und bleiben unklassifiziert. */ private static final int MIN_RANDZEILEN = 5; private FontgroessenFilter() {} @@ -106,9 +104,13 @@ final class FontgroessenFilter { } /** - * Ersetzt die End-X-Metadaten der Zeilen durch die Umbruch-Klassifikation: Zeilen, die am - * lokalen rechten Rand enden, erhalten {@link TextBereiniger#WEICHES_ZEILENENDE}, deutlich - * davor endende {@link TextBereiniger#HARTES_ZEILENENDE}; alles andere bleibt unmarkiert. + * Ersetzt die End-X-Metadaten der Zeilen durch die Umbruch-Klassifikation: Zeilen, die an einem + * lokalen Satzspiegelrand enden (Ausrichtungs-Cluster aus mindestens {@link #MIN_RANDZEILEN} + * gleich endenden Fensterzeilen), erhalten {@link TextBereiniger#WEICHES_ZEILENENDE}; Zeilen, + * die deutlich vor einem solchen Rand enden, {@link TextBereiniger#HARTES_ZEILENENDE}; alles + * andere bleibt unmarkiert. Cluster statt Perzentil, weil ein Fenster am Spaltenwechsel des + * zweispaltigen alten BGBl beide Spaltenränder enthält — maßgeblich ist der Rand, an dem die + * Zeile selbst ausgerichtet ist bzw. der nächste oberhalb ihres Endes. */ private static String klassifiziereZeilenenden(String text) { var zeilen = text.split("\n", -1); @@ -139,7 +141,8 @@ final class FontgroessenFilter { } for (int i = 0; i < zeilen.length; i++) { - if (Float.isNaN(endX[i])) { + float x = endX[i]; + if (Float.isNaN(x)) { continue; } var fenster = new ArrayList<Float>(); @@ -150,26 +153,34 @@ final class FontgroessenFilter { fenster.add(endX[j]); } } - fenster.sort(null); - float rand = fenster.get(Math.min((int) (fenster.size() * RAND_PERZENTIL), fenster.size() - 1)); - int randZeilen = 0; - for (float x : fenster) { - if (x >= rand - WEICH_TOLERANZ_PT) { - randZeilen++; - } - } - if (randZeilen < MIN_RANDZEILEN) { - continue; // kein Blocksatz-Nachweis im Umfeld — nicht klassifizierbar - } - if (endX[i] >= rand - WEICH_TOLERANZ_PT) { + if (istCluster(fenster, x)) { + // Die Zeile endet an einem Satzspiegelrand → automatischer Blocksatz-Umbruch. zeilen[i] = zeilen[i] + TextBereiniger.WEICHES_ZEILENENDE; - } else if (endX[i] < rand - HART_ABSTAND_PT) { - zeilen[i] = zeilen[i] + TextBereiniger.HARTES_ZEILENENDE; + continue; + } + // Gibt es deutlich oberhalb des Zeilenendes einen Satzspiegelrand, wäre dort noch Platz + // gewesen → das Zeilenende ist bewusst gesetzt. + for (float v : fenster) { + if (v >= x + HART_ABSTAND_PT && istCluster(fenster, v)) { + zeilen[i] = zeilen[i] + TextBereiniger.HARTES_ZEILENENDE; + break; + } } } return String.join("\n", zeilen); } + /** Enden mindestens {@link #MIN_RANDZEILEN} der Fensterzeilen gleich ausgerichtet bei {@code x}? */ + private static boolean istCluster(List<Float> fenster, float x) { + int anzahl = 0; + for (float v : fenster) { + if (Math.abs(v - x) <= CLUSTER_TOLERANZ_PT) { + anzahl++; + } + } + return anzahl >= MIN_RANDZEILEN; + } + /** Anteil der Seitenhöhe, unterhalb dessen Brotschrift-Text als Seitenfuß (Kolumnentitel) * gilt und die Fußnotengrenze nicht nach unten ziehen darf. */ private static final float SEITENFUSS_BEREICH = 0.92f; 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 1355877..8a7b6ae 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -107,6 +107,17 @@ public final class TextBereiniger { private static final Pattern AUFZAEHLUNGSMARKER = Pattern.compile("(\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s"); + /** Zeilen, die eigenständige Struktur-Anker des Parsers sind („Artikel 2“, „§ 19“, allein + * stehende Gliederungs-Bezeichnungen) und nie mit Nachbarzeilen zusammengezogen werden dürfen — + * auch dann nicht, wenn die Geometrie sie für einen Blocksatz-Umbruch hält: gleich breite, + * zentrierte Überschriften in Serie (etwa die Artikel-Überschriften der Folgeänderungen eines + * Entwurfs) bilden ein Schein-Ausrichtungs-Cluster. */ + private static final Pattern STRUKTURZEILE = + Pattern.compile( + "^(?:(?:Artikel|Teil|Abschnitt|Unterabschnitt|Kapitel|Titel|Buch)\\s+\\d+[a-z]?" + + "|§\\s*\\d+[a-z]?" + + "|(?:Anlage|Anhang)(?:\\s+\\d+[a-z]?)?)$"); + /** Perzentil der Zeilenlängen, das als „volle Spaltenbreite“ gilt (siehe {@link #verbindeUmbrueche}). */ private static final double VOLLZEILE_PERZENTIL = 0.9; @@ -426,12 +437,17 @@ public final class TextBereiniger { if (erste) { sb.append(text); erste = false; - } else if (vorherWeich && !AUFZAEHLUNGSMARKER.matcher(gestrippt).lookingAt()) { + } else if (vorherWeich + && !AUFZAEHLUNGSMARKER.matcher(gestrippt).lookingAt() + && !STRUKTURZEILE.matcher(gestrippt).matches()) { sb.append(' ').append(gestrippt); } else { sb.append('\n').append(text); } - vorherWeich = zeile.umbruch() == Umbruch.WEICH && !text.isBlank(); + vorherWeich = + zeile.umbruch() == Umbruch.WEICH + && !text.isBlank() + && !STRUKTURZEILE.matcher(gestrippt).matches(); } return sb.toString(); } diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index c5a4bbb..ac4dbf2 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -526,12 +526,7 @@ public final class BefehlAnwender { befehl, (text, bereich) -> { var einrueckung = einrueckungVon(text, bereich.von()); - var ersatz = - normalisiereZitatText(befehl.text()) - .lines() - .map(zeile -> einrueckung + zeile.strip()) - .reduce((a, b) -> a + "\n" + b) - .orElse(""); + var ersatz = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung); return TextErgebnis.ok( text.substring(0, bereich.von()) + ersatz + text.substring(bereich.bis())); }); @@ -644,12 +639,7 @@ public final class BefehlAnwender { var absatz = absaetze.get(f1.absatzIndex()); var text = absatz.text(); var einrueckung = einrueckungVon(text, von); - var ersatz = - normalisiereZitatText(befehl.text()) - .lines() - .map(zeile -> einrueckung + zeile.strip()) - .reduce((a, b) -> a + "\n" + b) - .orElse(""); + var ersatz = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung); absaetze.set(f1.absatzIndex(), absatz.mitText(text.substring(0, von) + ersatz + text.substring(bis))); normen.set(f1.normIndex(), norm.mitAbsaetzen(absaetze)); return angewandt(befehl, norm.enbez()); @@ -734,12 +724,7 @@ public final class BefehlAnwender { int position = befehl.vorher() ? bereich.von() : bereich.bis(); // Der Einfügeblock darf mehrere Einheiten enthalten („die Nummern 4a bis 4c“); // jede Aufzählungszeile des Zitats bleibt eine eigene Zeile. - var block = - normalisiereZitatText(befehl.text()) - .lines() - .map(zeile -> einrueckung + zeile.strip()) - .reduce((a, b) -> a + "\n" + b) - .orElse(""); + var block = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung); return TextErgebnis.ok( befehl.vorher() ? text.substring(0, position) + block + "\n" + text.substring(position) @@ -1268,6 +1253,10 @@ public final class BefehlAnwender { * beabsichtigt (z.B. die Kurzüberschrift über einer hängend eingerückten Definition im * UWG-Anhang) und müssen dieselbe Form erhalten wie beim Flatten des Stammgesetz-XML. */ + /** Eine Zeile, die mit einem Aufzählungsmarker beginnt („3. “, „d) “). */ + private static final Pattern AUFZAEHLUNGSZEILE = + Pattern.compile("^(\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s.*"); + private static String normalisiereZitatText(String text) { var zeilen = text.split("\n"); var sb = new StringBuilder(); @@ -1283,7 +1272,10 @@ public final class BefehlAnwender { } if (sb.length() == 0) { sb.append(gestutzt); - } else if (gestutzt.matches("^(\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s.*")) { + if (AUFZAEHLUNGSZEILE.matcher(gestutzt).matches()) { + fortsetzungsEinzug = " "; + } + } else if (AUFZAEHLUNGSZEILE.matcher(gestutzt).matches()) { // Aufzählungspunkt: eigene Zeile mit Einzug. sb.append("\n ").append(gestutzt); fortsetzungsEinzug = " "; @@ -1294,6 +1286,30 @@ public final class BefehlAnwender { return sb.toString(); } + /** + * Rückt die kanonischen Zitatzeilen auf die Ziel-Einrückung um: Aufzählungszeilen (und die + * erste Zeile) auf {@code einrueckung}, Fortsetzungszeilen — etwa der Definitionstext unter + * einer Kurzüberschrift — zwei Zeichen tiefer, damit sie Kindzeilen der Einheit bleiben + * (dieselbe Form, die der ContentFlattener aus dem Stammgesetz-XML erzeugt). + */ + private static String rueckeZitatEin(String zitat, String einrueckung) { + var sb = new StringBuilder(); + boolean erste = true; + for (var zeile : zitat.split("\n", -1)) { + var inhalt = zeile.strip(); + if (!erste) { + sb.append('\n'); + } + sb.append(einrueckung); + if (!erste && !AUFZAEHLUNGSZEILE.matcher(inhalt).matches()) { + sb.append(" "); + } + sb.append(inhalt); + erste = false; + } + return sb.toString(); + } + private static AngewandteAenderung angewandt(Aenderungsbefehl befehl, String enbez) { return new AngewandteAenderung( befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(List.of(enbez))); diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index a395fa1..0b7820d 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -88,6 +88,13 @@ class EndToEndTest { // werden (kurze Stichwort-Zeile vor hängend eingerückter Definition, kein Wort wird getrennt). assertThat(text).contains("Nachhaltigkeitssiegels\ndas Anbringen"); assertThat(text).doesNotContain("Nachhaltigkeitssiegelsdas"); + // Dank geometrischer Umbruch-Klassifikation gilt das auch für Stichwort-Zeilen, die fast die + // volle Spaltenbreite erreichen (Nummer 4c) … + assertThat(text).contains("Treibhausgasemissionen\ndas Treffen"); + assertThat(text).doesNotContain("Treibhausgasemissionendas"); + // … und für markerlose Zeilenenden vor einer Aufzählungs-Folgezeile („und“ + „d)“). + assertThat(text).contains("vorgesehen und\nd) die Überwachung"); + assertThat(text).doesNotContain("undd)"); var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null); assertThat(parseErgebnis.artikel()).containsExactly("1"); @@ -100,6 +107,14 @@ class EndToEndTest { var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); assertThat(anwendung.anzahlManuell()).isZero(); assertThat(anwendung.anzahlAngewandt()).isEqualTo(parseErgebnis.befehle().size()); + + // Kurzüberschrift und Definitionstext stehen in der angewandten Fassung auf eigenen Zeilen — + // sowohl bei XML-stämmigen (Nummer 22) als auch bei PDF-stämmigen (Nummer 2a) Anhang-Nummern. + var anhangNeu = anwendung.neu().norm("Anhang").orElseThrow(); + assertThat(anhangNeu.gesamtText()) + .contains("Irreführung über Unternehmereigenschaft\n die unwahre Angabe") + .contains("unzulässiges Anbringen eines Nachhaltigkeitssiegels\n das Anbringen"); + var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false); assertThat(HtmlRenderer.rendere(synopse, "E2E-Test")).contains("Neue Fassung"); } 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 4ca86cf..8a7cf12 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java @@ -121,6 +121,57 @@ class TextBereinigerTest { } @Test + void ziehtMarkerlosNichtVorAufzaehlungsmarkerZusammen() { + // Ein Marker eröffnet eine bewusste Strukturzeile — auch wenn die Vorzeile markerlos mit + // einem Buchstaben endet und die Folgezeile klein beginnt („…und“ + „d) die Überwachung“). + var roh = + "es ist der Entzug der Verwendung im Fall von Verstößen gegen die Anforderungen" + + " vorgesehen und\n" + + "d) die Überwachung der Einhaltung der Anforderungen durch einen Dritten erfolgt. \n" + + "Nächste Zeile endet mit Leerzeichen. "; + + assertThat(TextBereiniger.bereinige(roh)).contains("und\nd) die Überwachung"); + } + + @Test + void ziehtMarkerlosNichtBeiGeometrischHartemZeilenendeZusammen() { + // Volle Spaltenbreite in Zeichen, aber laut Geometrie-Marker des FontgroessenFilters + // deutlich vor dem Rand endend (lange Stichwort-Zeile wie Nummer 4c im UWG-Anhang): + // der Umbruch ist bewusst und bleibt erhalten. + var roh = + "4c. Aussagen zu Umweltauswirkungen bei Kompensation von Treibhausgasemissionen\uE000\n" + + "das Treffen einer Aussage, die sich auf die Kompensation von" + + " Treibhausgasemissionen gründet. "; + + assertThat(TextBereiniger.bereinige(roh)).contains("Treibhausgasemissionen\ndas Treffen"); + } + + @Test + void reflowtGeometrischWeicheUmbruecheUndErhaeltHarte() { + var roh = + "Erste Zeile des Absatzes wird \uE001\n" + + "fortgesetzt und endet hier. \uE000\n" + + "Nächste eigene Zeile. \uE000"; + + assertThat(TextBereiniger.bereinige(roh)) + .isEqualTo("Erste Zeile des Absatzes wird fortgesetzt und endet hier.\nNächste eigene Zeile."); + } + + @Test + void strukturzeilenBleibenTrotzWeicherKlassifikationEigeneZeilen() { + // Gleich breite, zentrierte Artikel-Überschriften in Serie bilden ein Schein- + // Ausrichtungs-Cluster und werden fälschlich als weich klassifiziert — der Parser braucht + // sie aber allein auf der Zeile (teileInArtikel). + var roh = + "Artikel 2\uE001\n" + + "Inkrafttreten\uE000\n" + + "Dieses Gesetz tritt am Tag nach der Verkündung in Kraft. \uE000"; + + assertThat(TextBereiniger.bereinige(roh)) + .isEqualTo("Artikel 2\nInkrafttreten\nDieses Gesetz tritt am Tag nach der Verkündung in Kraft."); + } + + @Test void repariertInvertierteZitatzeichenAnAbsatzmarkern() { // BMJV-Vorlagen zeichnen das hängende „ nach dem Absatzmarker bzw. der Paragraphenangabe. assertThat(TextBereiniger.bereinige("(1) „ Ungeachtet des § 8 gilt.“")) diff --git a/src/test/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoaderTest.java b/src/test/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoaderTest.java index b3e3f32..607f24f 100644 --- a/src/test/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoaderTest.java +++ b/src/test/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoaderTest.java @@ -19,7 +19,7 @@ class GiiXmlLoaderTest { assertThat(gesetz.langue()).isEqualTo("Gesetz zur Erprobung des ÄndGgners"); assertThat(gesetz.kurzue()).isEqualTo("Testgesetz"); // Die Rahmen-Norm ohne enbez wird übersprungen. - assertThat(gesetz.normen()).hasSize(3); + assertThat(gesetz.normen()).hasSize(4); assertThat(gesetz.normen().get(0).enbez()).isEqualTo("Inhaltsübersicht"); assertThat(gesetz.norm("§ 1")).isPresent(); assertThat(gesetz.norm("§ 3")).isEmpty(); @@ -59,6 +59,26 @@ class GiiXmlLoaderTest { } @Test + void trenntKurzueberschriftUndDefinitionInAufzaehlungen() throws Exception { + var gesetz = new GiiXmlLoader().load(fixture()); + var anhang = gesetz.norm("Anhang").orElseThrow().absaetze().get(0).text(); + + // Zwei <LA>-Geschwister in einem <DD> (Kurzüberschrift + Definitionstext, wie im UWG-Anhang + // oder in § 2 IfSG) sind eigene Zeilen — nicht nahtlos verklebter Fließtext. Die Folgezeile + // ist tiefer eingerückt als die Aufzählungszeile, damit die Stellenauflösung sie als + // Kindzeile der Einheit erkennt. + assertThat(anhang).doesNotContain("Erprobungdie"); + assertThat(anhang) + .contains( + "1. Irreführung über die Erprobung\n" + + " die unwahre Angabe, die Erprobung sei abgeschlossen;"); + // Auch mit geschachtelter Aufzählung im Definitionstext bleibt die Zeilenstruktur erhalten. + assertThat(anhang) + .contains(" 2. Verheimlichung von Prüfschritten\n das Verschweigen, dass") + .contains(" a) Zwischenschritte oder"); + } + + @Test void flattetTabellenZuZeilen() throws Exception { var gesetz = new GiiXmlLoader().load(fixture()); var inhaltsuebersicht = gesetz.norm("Inhaltsübersicht").orElseThrow(); diff --git a/src/test/resources/eu/mulk/aendggner/mini-gii.xml b/src/test/resources/eu/mulk/aendggner/mini-gii.xml index 866f81b..8ae0418 100644 --- a/src/test/resources/eu/mulk/aendggner/mini-gii.xml +++ b/src/test/resources/eu/mulk/aendggner/mini-gii.xml @@ -78,4 +78,18 @@ </text> </textdaten> </norm> + <norm builddate="20201120213007" doknr="BJNR000000000BJNE000400000"> + <metadaten> + <jurabk>TestG</jurabk> + <enbez>Anhang</enbez> + <titel format="XML">(zu § 1)</titel> + </metadaten> + <textdaten> + <text format="XML"> + <Content> + <P><DL Font="normal" Type="arabic"><DT>1.</DT><DD Font="normal"><LA Size="normal">Irreführung über die Erprobung</LA><LA Size="normal">die unwahre Angabe, die Erprobung sei abgeschlossen;</LA></DD><DT>2.</DT><DD Font="normal"><LA Size="normal">Verheimlichung von Prüfschritten</LA><LA Size="normal">das Verschweigen, dass <DL Font="normal" Type="alpha"><DT>a)</DT><DD Font="normal"><LA Size="normal">Zwischenschritte oder</LA></DD><DT>b)</DT><DD Font="normal"><LA Size="normal">Endschritte ausgelassen wurden.</LA></DD></DL></LA></DD></DL></P> + </Content> + </text> + </textdaten> + </norm> </dokumente> |
