aboutsummaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-18 08:57:22 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-19 09:00:01 +0200
commitf20828ffd8ea9d554566636a9c53a3abd57507c5 (patch)
tree6d795f85206b7b427fe92fc1c89df5a2fba07595 /src/test
parent99511a961d741e2dc06113cf5f24180bfea2b26b (diff)
Classify line ends by alignment clusters and canonicalize definition lists
The percentile-based right-margin estimate from the previous commit misclassified two real-world layouts: at the column switch of the two-column old-BGBl format a window contains both column margins, so the 90th percentile returned the right column's margin and marked every left-column soft wrap as deliberate (vetoing hyphen mending, IfSG 2020: "Impf- surveillance"); and series of equally wide centered "Artikel N" headings in draft bills formed the top of their window's distribution, so they counted as full-width and were reflowed into the following line, breaking teileInArtikel's line anchor (ProdHaftG RegE). Classify against alignment clusters instead: a line end is soft when at least five window lines end within 4 pt of it (the justified block's own margin), hard when such a cluster runs at least 10 pt above it, unclassified otherwise. Structure anchors ("Artikel 2", "§ 19", bare Gliederung labels) are always exempt from reflow since equal-width heading series still form sham clusters. Also flush the pending line end-X at page ends — without it, the first line of the next page inherited the previous page's footer geometry and was misclassified (UWG: "Artikel 2" swallowed the closing provisions). Markerless joins additionally never cross into a following enumeration marker line ("...vorgesehen und" + "d) die Überwachung" no longer glues to "undd)"), a text-level veto that also works without geometry. On the base-law side, ContentFlattener now separates sibling <LA> elements within a <DD> — the short-label/definition pairs of the UWG Anhang and § 2 IfSG were previously glued without any separator ("Irreführung über Unternehmereigenschaftdie unwahre Angabe...") in both synopsis columns. The continuation line is indented two spaces deeper than its enumeration line so StellenAufloeser.zeilenBlock keeps it inside the unit's block ("Anhang Nummer 31 Buchstabe b"). BefehlAnwender writes the same canonical shape when inserting or recasting quoted units (rueckeZitatEin), so XML-derived and PDF-derived items agree: " 2a. Stichwort" + " Definitionstext". Verified by mvnw verify (179 tests, 7 new) and a before/after sweep of all 21 sample law/amendment combinations: applied/manual counts are unchanged throughout (UWG 19/0, GEG-BGBl 66/53, GModG-RegE 77/20, IfSG-0645 42/24, ...), no marker characters leak into the HTML output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Change-Id: Ib7faae1bc5c59bda83f648a579af16a4c77e4285
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java15
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java51
-rw-r--r--src/test/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoaderTest.java22
-rw-r--r--src/test/resources/eu/mulk/aendggner/mini-gii.xml14
4 files changed, 101 insertions, 1 deletions
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>