aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/FontgroessenFilter.java77
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java20
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java54
-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
7 files changed, 198 insertions, 55 deletions
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>