aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/eu
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-22 16:41:57 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-22 16:41:57 +0200
commit14ec52a1070285a12a6bc5a0f4859996d468b59e (patch)
tree8781a201ce4cc388c6e6283dde48b4fae3c12502 /src/main/java/eu
parent75583b2e502aa50dfb616650ac0938d15dab826b (diff)
Die Einrückung trägt die Gliederung, nicht bloß das Satzbild
Die vorangegangene Fassung hatte zwei Mängel festgehalten, aber nicht behoben: Der Block aus „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ trat ans Ende des Absatzes statt hinter die Nr. 4, und die Heilung des Weißraums nach einer Streichung verkürzte die Einrückung der Aufzählungszeilen. Beide wiegen schwerer als alles sonst Offene, denn sie führen zu einem falschen Ergebnis, ohne es anzuzeigen: Der Befehl gilt als angewandt, der Abschnitt „Manuell prüfen“ schweigt, und allein der Wortlaut weicht ab. Damit war das Leitprinzip des Erzeugnisses an dieser Stelle verfehlt. Bei der Behebung hat sich ergeben, dass die beiden nicht nebeneinanderstanden, sondern eine Ursache teilten. Die Neufassung einer Nummer oder eines Buchstabens setzte ihren Wortlaut ohne Einrückung, also in die erste Spalte. Der Zeilenblock einer Aufzählungsmarke reicht aber so weit, wie tiefer eingerückt wird; eine in der ersten Spalte stehende Zeile nimmt daher den gesamten Rest des Absatzes als ihre Kindzeilen an. Der nachfolgende Einfügebefehl fand seinen Anker mithin am Ende des Absatzes und stand richtig, wo er falsch stand. Die Einrückung ist im kanonischen Klartext kein Satzbild, sondern die Zugehörigkeit einer Zeile zu ihrer Einheit; die Neufassung setzt sie nunmehr wie die Ersetzung, und die Heilung nach einer Streichung glättet allein die Naht, an der Text herausgenommen wurde. Die zeilenweise Gegenprobe an der amtlichen Nachfassung hat darüber hinaus zwei Abweichungen gezeigt, die keine Mängel der Reihenfolge waren. Zum einen hielt der leere Platzhalter „7. (aufgehoben)“ eine Bezeichnung, die der eingefügte Block neu vergibt; er weicht fortan, wie er es bei der Umnummerierung auf eine weggefallene Bezeichnung längst tut. Verdrängt wird dabei nur ein Platzhalter, niemals ein Wortlaut — der Platzhalter des Absatzes 2, den kein Befehl anrührt, bleibt stehen. Zum anderen führt der Satz des Gesetzblatts zwischen der Marke einer Aufzählungszeile und ihrem Text die Tabulatorspalte des Setzers, der kanonische Klartext dagegen ein Leerzeichen; ohne dessen Rückführung trügen gerade die neu gesetzten Zeilen ein anderes Satzbild als die unberührten. Artikel 56 Absatz 1 des Bayerischen Jagdgesetzes gleicht damit in Aufbau und Wortlaut der amtlichen Nachfassung. Die beiden Zusicherungen des Akzeptanztests, die den Mangel festhielten, sind umgekehrt; drei Prüfungen der zugrunde liegenden Regeln treten hinzu. Dreihundertfünfzehn Testfälle bestehen, und keine der gepinnten Zahlen der übrigen Belegfälle hat sich geändert. Die Browserfassung ist übersetzt worden und bleibt auslieferbar; ihr Lauf im Browser ist mangels verbundener Erweiterung nicht in Augenschein genommen worden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I89b2fafa3e2185dbd6abfe0aa6b7bbbd04f6828f
Diffstat (limited to 'src/main/java/eu')
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java123
1 files changed, 116 insertions, 7 deletions
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 32350ff..73745a6 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -716,10 +716,11 @@ public final class BefehlAnwender {
return TextErgebnis.fehler(
"„" + befehl.woerter() + "“ kommt " + anzahl + "-mal vor.");
}
+ int naht = text.indexOf(befehl.woerter());
return TextErgebnis.ok(
- text.replace(befehl.woerter(), "")
- .replaceAll(" +", " ")
- .replaceAll(" ([,;.])", "$1"));
+ heileNaht(
+ text.substring(0, naht) + text.substring(naht + befehl.woerter().length()),
+ naht));
}));
}
@@ -833,7 +834,27 @@ public final class BefehlAnwender {
return angewandt(befehl, norm.enbez());
}
- // Neufassung eines Satzes / einer Nummer / eines Buchstabens: Bereich ersetzen.
+ // Neufassung einer Nummer / eines Buchstabens: Der neue Wortlaut tritt an die Stelle der
+ // alten Einheit — und zwar auf deren Einrückung. Ohne sie stünde die neugefasste Zeile in
+ // Spalte 0, und der Zeilenblock ihrer Marke reichte fortan bis ans Absatzende, weil er so
+ // weit reicht, wie tiefer eingerückt wird (StellenAufloeser.zeilenBlock): ein späterer
+ // Befehl „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ träte dann hinter die
+ // letzte Nummer des Absatzes statt hinter die Nr. 4.
+ if (feinsteIstAufzaehlung(befehl.stelle())) {
+ return bearbeiteBereich(
+ normen,
+ befehl,
+ (text, bereich) ->
+ TextErgebnis.ok(
+ text.substring(0, bereich.von())
+ + rueckeZitatEin(
+ normalisiereZitatText(befehl.neuerText()),
+ einrueckungVon(text, bereich.von()))
+ + text.substring(bereich.bis())));
+ }
+
+ // Neufassung eines Satzes / Halbsatzes: Bereich ersetzen. Eine Einrückung gibt es hier
+ // nicht — der Bereich beginnt mitten in der Zeile.
return bearbeiteText(
normen, befehl, text -> TextErgebnis.ok(normalisiereZitatText(befehl.neuerText())));
}
@@ -1105,10 +1126,19 @@ public final class BefehlAnwender {
// Der Einfügeblock darf mehrere Einheiten enthalten („die Nummern 4a bis 4c“);
// jede Aufzählungszeile des Zitats bleibt eine eigene Zeile.
var block = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung);
- return TextErgebnis.ok(
+ var neuerText =
befehl.vorher()
? text.substring(0, position) + block + "\n" + text.substring(position)
- : text.substring(0, position) + "\n" + block + text.substring(position));
+ : text.substring(0, position) + "\n" + block + text.substring(position);
+ // Vergibt der Block eine Bezeichnung, die ein leerer Platzhalter der Altfassung
+ // noch hält („7. (aufgehoben)“), so weicht dieser: Zwei Einheiten gleicher
+ // Bezeichnung kann es nicht geben, und Inhalt geht dabei nicht verloren. Es ist
+ // dieselbe Regel, die schon bei der Umnummerierung auf eine weggefallene
+ // Bezeichnung gilt, und so zeigt es die amtliche Nachfassung.
+ for (var marke : platzhalterMarken(block, befehl.ebene())) {
+ neuerText = entferneWeggefallenenPlatzhalter(neuerText, marke);
+ }
+ return TextErgebnis.ok(neuerText);
});
};
}
@@ -1421,6 +1451,36 @@ public final class BefehlAnwender {
};
}
+ private static final Pattern LEERER_PLATZHALTER =
+ Pattern.compile("\\((?:weggefallen|gegenstandslos|aufgehoben)\\)");
+
+ /**
+ * Die Aufzählungsmarken, die ein eingefügter Block vergibt („5.“, „6.“, „7.“) — ohne solche, die
+ * er selbst als leeren Platzhalter führt, denn ein Platzhalter verdrängt keinen Platzhalter.
+ */
+ private static List<String> platzhalterMarken(String block, Ebene ebene) {
+ var muster =
+ switch (ebene) {
+ case NUMMER -> NUMMER_MARKER;
+ case BUCHSTABE -> BUCHSTABE_MARKER;
+ default -> null;
+ };
+ if (muster == null) {
+ return List.of();
+ }
+ var marken = new ArrayList<String>();
+ var marker = muster.matcher(block);
+ while (marker.find()) {
+ int zeilenEnde = block.indexOf('\n', marker.end());
+ var rest = block.substring(marker.end(), zeilenEnde < 0 ? block.length() : zeilenEnde);
+ if (LEERER_PLATZHALTER.matcher(rest.strip()).matches()) {
+ continue;
+ }
+ marken.add(marker.group(1) + (ebene == Ebene.BUCHSTABE ? ")" : "."));
+ }
+ return marken;
+ }
+
/**
* Entfernt die weggefallene Aufzählungszeile mit der gegebenen Marke — die soeben umbenannte
* Zeile bleibt stehen, weil nur ein leerer Platzhalter getroffen wird.
@@ -1812,6 +1872,50 @@ public final class BefehlAnwender {
|| k instanceof Stelle.BuchstabeNr);
}
+ /**
+ * Wahr, wenn die feinste Komponente der Stelle eine Aufzählungseinheit ist (Nummer/Buchstabe).
+ */
+ private static boolean feinsteIstAufzaehlung(Stelle stelle) {
+ var komponenten = stelle.komponenten();
+ if (komponenten.isEmpty()) {
+ return false;
+ }
+ var feinste = komponenten.get(komponenten.size() - 1);
+ return feinste instanceof Stelle.NummerNr || feinste instanceof Stelle.BuchstabeNr;
+ }
+
+ /**
+ * Heilt den Weißraum an der Stelle, an der eine Streichung Text herausgenommen hat: Aus zwei
+ * Leerzeichen wird eines, vor einem Satzzeichen bleibt keines. Geheilt wird ausdrücklich nur die
+ * Naht und nicht der ganze Zieltext — dieser ist bei einer Stelle ohne Fein-Komponente der
+ * gesamte Absatz, und eine Heilung über ihn hinweg fräße die Einrückung der Aufzählungszeilen:
+ * Aus den zwei Leerzeichen vor jeder Marke würde eines. Traf die Streichung den Zeilenanfang, so
+ * ist der Weißraum vor der Naht Einrückung und bleibt unangetastet.
+ */
+ private static String heileNaht(String text, int naht) {
+ int von = naht;
+ while (von > 0 && text.charAt(von - 1) == ' ') {
+ von--;
+ }
+ int bis = naht;
+ while (bis < text.length() && text.charAt(bis) == ' ') {
+ bis++;
+ }
+ if (bis == von) {
+ return text;
+ }
+ if (von == 0 || text.charAt(von - 1) == '\n') {
+ return text.substring(0, naht) + text.substring(bis);
+ }
+ if (bis < text.length() && ",;.".indexOf(text.charAt(bis)) >= 0) {
+ return text.substring(0, von) + text.substring(bis);
+ }
+ if (bis - von == 1) {
+ return text;
+ }
+ return text.substring(0, von) + " " + text.substring(bis);
+ }
+
private static @Nullable String labelVon(Stelle stelle) {
for (var komponente : stelle.komponenten().reversed()) {
if (komponente instanceof Stelle.NummerNr n) {
@@ -1969,6 +2073,11 @@ public final class BefehlAnwender {
private static final Pattern AUFZAEHLUNGSZEILE =
Pattern.compile("^(\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s.*");
+ // Der Zwischenraum zwischen der Aufzählungsmarke und ihrem Text: im Gesetzblattsatz steht dort
+ // die Marken-Tabulatorspalte, im kanonischen Klartext ein einzelnes Leerzeichen.
+ private static final Pattern MARKEN_ZWISCHENRAUM =
+ Pattern.compile("^(\\d+[a-z]?\\.|[a-z]{1,3}\\))[ \\t]+");
+
private static String normalisiereZitatText(String text) {
var zeilen = text.split("\n");
var sb = new StringBuilder();
@@ -1978,7 +2087,7 @@ public final class BefehlAnwender {
// Stellenauflösung sie als Kindzeilen der Einheit erkennt.
var fortsetzungsEinzug = "";
for (var zeile : zeilen) {
- var gestutzt = zeile.strip();
+ var gestutzt = MARKEN_ZWISCHENRAUM.matcher(zeile.strip()).replaceFirst("$1 ");
if (gestutzt.isEmpty()) {
continue;
}