diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/AendGgner.java | 8 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/Pipeline.java | 25 | ||||
| -rw-r--r-- | src/main/resources/eu/mulk/aendggner/web/app.js | 34 | ||||
| -rw-r--r-- | src/main/resources/eu/mulk/aendggner/web/index.html | 17 | ||||
| -rw-r--r-- | src/main/resources/eu/mulk/aendggner/web/style.css | 24 | ||||
| -rw-r--r-- | src/main/resources/eu/mulk/aendggner/web/worker.js | 5 | ||||
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/EndToEndTest.java | 28 | ||||
| -rw-r--r-- | src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java | 18 |
8 files changed, 141 insertions, 18 deletions
diff --git a/src/main/java/eu/mulk/aendggner/AendGgner.java b/src/main/java/eu/mulk/aendggner/AendGgner.java index a12515a..58b0d50 100644 --- a/src/main/java/eu/mulk/aendggner/AendGgner.java +++ b/src/main/java/eu/mulk/aendggner/AendGgner.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.concurrent.Callable; @@ -129,12 +130,11 @@ public class AendGgner implements Callable<Integer> { } if (extractOnly) { - var gesetz = Pipeline.ladeStammgesetz(baseFile); - var extraktor = new PatchTextExtraktor(Pipeline.superskriptModus(gesetz)); + var quellen = new ArrayList<Quelle>(); for (var file : patches) { - var text = extraktor.extrahiere(file); - System.out.println(raw ? text : TextBereiniger.bereinige(text)); + quellen.add(Quelle.lies(file)); } + System.out.print(Pipeline.extrahiereText(Quelle.lies(baseFile), quellen, raw)); return 0; } diff --git a/src/main/java/eu/mulk/aendggner/Pipeline.java b/src/main/java/eu/mulk/aendggner/Pipeline.java index 76b492f..7949e7f 100644 --- a/src/main/java/eu/mulk/aendggner/Pipeline.java +++ b/src/main/java/eu/mulk/aendggner/Pipeline.java @@ -275,6 +275,31 @@ public final class Pipeline { return dokument.kopf().art().istEntwurfsfassung(); } + /** + * Der Text der Änderungsdokumente, so wie das Erzeugnis ihn liest — der Notausgang des § 6 Absatz + * 2 des Handbuchs. + * + * <p>Bleibt die PDF-Aufbereitung im Einzelfall fehlerhaft, so ist nicht das Ergebnis zu + * beargwöhnen, sondern der Text nachzusehen, von Hand zu berichtigen und als Klartextdatei wieder + * einzuspeisen. Dass dieser Weg beiden Fassungen offensteht, ist kein Zierat: Wer im Browser + * arbeitet, hat sonst keine Möglichkeit, einem unerklärlichen Rest auf den Grund zu gehen. + * + * <p>Das Stammgesetz wird auch hier gebraucht, denn seine Schreibweise bestimmt den + * Superskriptmodus der Textgewinnung ({@link #superskriptModus}). + * + * @param roh die Bereinigung übergehen; nur zur Fehlersuche an der Textgewinnung selbst. + */ + public static String extrahiereText(Quelle baseFile, List<Quelle> patches, boolean roh) + throws Exception { + var extraktor = new PatchTextExtraktor(superskriptModus(ladeStammgesetz(baseFile))); + var text = new StringBuilder(); + for (var patch : patches) { + var roher = extraktor.extrahiere(patch); + text.append(roh ? roher : TextBereiniger.bereinige(roher)).append('\n'); + } + return text.toString(); + } + /** Bequemlichkeit für Befehlszeile und Tests; im Browser gibt es keine {@link Path}e. */ static Gesetz ladeStammgesetz(Path baseFile) throws Exception { return ladeStammgesetz(Quelle.lies(baseFile)); diff --git a/src/main/resources/eu/mulk/aendggner/web/app.js b/src/main/resources/eu/mulk/aendggner/web/app.js index 3a2957c..97eca52 100644 --- a/src/main/resources/eu/mulk/aendggner/web/app.js +++ b/src/main/resources/eu/mulk/aendggner/web/app.js @@ -30,6 +30,15 @@ async function alsBase64(datei) { return { name: datei.name, base64: btoa(roh) }; } +/** Hängt der Meldung einen Verweis auf das Ergebnis an; ein Blob, kein Server. */ +function oeffne(inhalt, art, aufschrift) { + const verweis = document.createElement("a"); + verweis.href = URL.createObjectURL(new Blob([inhalt], { type: art })); + verweis.target = "_blank"; + verweis.textContent = aufschrift; + meldung.append(verweis); +} + formular.addEventListener("submit", async (ereignis) => { ereignis.preventDefault(); @@ -40,6 +49,9 @@ formular.addEventListener("submit", async (ereignis) => { return; } + const artikel = document.querySelector("#artikel").value.trim(); + const nurText = document.querySelector("#nurtext").checked; + knopf.disabled = true; zeige("Lade das Rechenwerk (einmalig einige Megabyte) und werte aus …", "arbeit"); @@ -60,7 +72,8 @@ formular.addEventListener("submit", async (ereignis) => { stamm, patches, vollstaendig: document.querySelector("#vollstaendig").checked, - artikel: null, + artikel: artikel === "" ? null : artikel, + nurText, }); }); @@ -69,22 +82,25 @@ formular.addEventListener("submit", async (ereignis) => { return; } - const blob = new Blob([ergebnis.html], { type: "text/html;charset=utf-8" }); - const adresse = URL.createObjectURL(blob); - // Kein window.open: Der Aufruf käme nach dem Warten auf den Worker und gälte dem Browser // nicht mehr als Nutzerhandlung — er würde als Popup blockiert. Stattdessen ein Link, der // sich öffnen und ebenso gut speichern lässt. + if (nurText) { + zeige( + `${ergebnis.text.length.toLocaleString("de-DE")} Zeichen gelesen; ` + + `keine Synopse erstellt. `, + "fertig", + ); + oeffne(ergebnis.text, "text/plain;charset=utf-8", "Gelesenen Text öffnen"); + return; + } + zeige( `${ergebnis.angewandt} Befehle angewandt, ${ergebnis.manuell} manuell zu prüfen, ` + `${ergebnis.normen} geänderte Normen. `, "fertig", ); - const verweis = document.createElement("a"); - verweis.href = adresse; - verweis.target = "_blank"; - verweis.textContent = "Synopse öffnen"; - meldung.append(verweis); + oeffne(ergebnis.html, "text/html;charset=utf-8", "Synopse öffnen"); } catch (e) { zeige("Verarbeitung fehlgeschlagen: " + (e && e.message ? e.message : e), "fehler"); } finally { diff --git a/src/main/resources/eu/mulk/aendggner/web/index.html b/src/main/resources/eu/mulk/aendggner/web/index.html index 680aa8c..edca09e 100644 --- a/src/main/resources/eu/mulk/aendggner/web/index.html +++ b/src/main/resources/eu/mulk/aendggner/web/index.html @@ -132,12 +132,29 @@ gewählten Reihenfolge nacheinander angewendet)</span></label> <input type="file" id="aenderung" name="aenderung" multiple required> </div> + <div class="feld"> + <label for="artikel">Anzuwendender Artikel + <span class="erl">(nur auszufüllen, wenn abweichend: ohne Angabe werden alle + Artikel angewandt, deren Einleitung das oben bezeichnete Stammgesetz + nennt)</span></label> + <input type="text" id="artikel" name="artikel" inputmode="numeric" + autocomplete="off" placeholder="z. B. 3"> + </div> <div class="feld ankreuzfeld"> <label> <input type="checkbox" id="vollstaendig" name="vollstaendig"> Unveränderte Vorschriften mit in die Synopse aufnehmen </label> </div> + <div class="feld ankreuzfeld"> + <label> + <input type="checkbox" id="nurtext" name="nurtext"> + Statt der Synopse nur den maschinell gelesenen Text der Änderungsdokumente ausgeben + </label> + <p class="erl nachsatz">Zur Nachschau, wenn ein Befehl unerklärlich unangewandt + bleibt: Der Text lässt sich sichern, von Hand berichtigen und als Klartextdatei + oben wieder einreichen.</p> + </div> </div> </div> diff --git a/src/main/resources/eu/mulk/aendggner/web/style.css b/src/main/resources/eu/mulk/aendggner/web/style.css index 36c2198..80479ff 100644 --- a/src/main/resources/eu/mulk/aendggner/web/style.css +++ b/src/main/resources/eu/mulk/aendggner/web/style.css @@ -215,6 +215,24 @@ input[type="file"] { font-size: 0.75rem; } +/* Die Eintragung von Hand steht auf einer Linie, wie im gedruckten Vordruck; sie ist kurz, + weil dort nur eine Artikelnummer hingehört. */ +input[type="text"] { + width: 8rem; + margin-top: 0.35rem; + padding: 0.1rem 0.2rem; + border: none; + border-bottom: 1px solid var(--linie); + background: transparent; + color: inherit; + font: inherit; + font-size: 0.8125rem; +} + +input[type="text"]::placeholder { + color: var(--muted); +} + /* Der Dateiwähler ist der einzige Knopf im Feld und trägt deshalb die Form der übrigen Eintragungsfelder: eckig, haarfein umrandet, weiß. */ input[type="file"]::file-selector-button { @@ -232,6 +250,12 @@ input[type="file"]::file-selector-button:hover { background: var(--grund); } +/* Der Nachsatz zu einem Ankreuzfeld steht unter dem Text, nicht neben ihm, und rückt bis + unter die Aufschrift ein. */ +.nachsatz { + margin: 0.15rem 0 0 1.6rem; +} + /* Das Ankreuzfeld steht vor dem Text und wird angekreuzt, nicht angehakt. */ .ankreuzfeld label { display: flex; diff --git a/src/main/resources/eu/mulk/aendggner/web/worker.js b/src/main/resources/eu/mulk/aendggner/web/worker.js index f20e873..827358e 100644 --- a/src/main/resources/eu/mulk/aendggner/web/worker.js +++ b/src/main/resources/eu/mulk/aendggner/web/worker.js @@ -19,7 +19,7 @@ const gestartet = GraalVM.run([], Object.assign(new GraalVM.Config(), { })); self.onmessage = async (nachricht) => { - const { stamm, patches, vollstaendig, artikel } = nachricht.data; + const { stamm, patches, vollstaendig, artikel, nurText } = nachricht.data; try { await gestartet; await bereit; @@ -28,9 +28,12 @@ self.onmessage = async (nachricht) => { patches, artikel: artikel ?? null, vollstaendig: Boolean(vollstaendig), + nurText: Boolean(nurText), }); if (ergebnis.fehler) { self.postMessage({ fehler: String(ergebnis.fehler) }); + } else if (nurText) { + self.postMessage({ text: String(ergebnis.text) }); } else { self.postMessage({ html: String(ergebnis.html), diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 2698fa5..fe98b84 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -99,6 +99,34 @@ class EndToEndTest { * die Rechnung vollständig auf: <b>75 von 75</b>, kein Rest. Das ist zugleich der Beleg, dass die * 27 nie ein Mangel des Werkzeugs waren. */ + /** + * Der Notausgang (§ 6 Absatz 2 des Handbuchs) gibt aus, was das Erzeugnis gelesen hat, und geht + * dabei denselben Weg wie die Synopse — auf ihn verlässt sich, wer einem unerklärlichen Rest + * nachgeht, und seit Welle 21 tut das auch die Browserfassung. + */ + @Test + void notausgangLiefertDenGelesenenText() throws Exception { + assumeTrue(Files.exists(STAMMGESETZ), "IfSG-Beispieldaten fehlen"); + assumeTrue(Files.exists(AENDERUNGSGESETZ), "BGBl-Beispiel-PDF fehlt"); + + var stamm = Quelle.lies(STAMMGESETZ); + var patches = List.of(Quelle.lies(AENDERUNGSGESETZ)); + + var bereinigt = Pipeline.extrahiereText(stamm, patches, false); + var roh = Pipeline.extrahiereText(stamm, patches, true); + + // Wortgleich mit dem, was der Parser bekommt — sonst führte der Notausgang in die Irre. + assertThat(bereinigt) + .isEqualTo( + TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(AENDERUNGSGESETZ)) + "\n"); + assertThat(bereinigt).contains("Nach § 28 wird folgender § 28a eingefügt"); + // Die Bereinigung nimmt fort, was der Satz beisteuert (Kolumnentitel, Trennstriche); der + // Rohtext ist deshalb länger und trägt den Kopf des Gesetzblatts noch. + assertThat(roh).hasSizeGreaterThan(bereinigt.length()); + assertThat(roh).contains("Bundesgesetzblatt"); + assertThat(bereinigt).doesNotContain("Bundesgesetzblatt Jahrgang 2020 Teil I Nr."); + } + @Test void ifsgGegenZeitrichtigenStamm() throws Exception { var xml = SAMPLEDATA.resolve("IfSG/BJNR104510000-2020.xml"); diff --git a/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java b/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java index 7df8ecb..c318f86 100644 --- a/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java +++ b/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java @@ -22,9 +22,10 @@ import org.graalvm.webimage.api.JSValue; * damit dieselbe {@link Pipeline} auf wie Befehlszeile und Tests. * * <p>Erwartet ein JS-Objekt <code>{stamm: {name, base64}, patches: [{name, base64}], artikel, - * vollstaendig}</code> und liefert <code>{html, angewandt, manuell, normen}</code> oder <code> - * {fehler}</code> zurück. Geworfen wird nichts: Eine Ausnahme im Wasm hinterlässt auf der JS-Seite - * nur einen unlesbaren Stapel, also wird jeder Fehler als Text zurückgereicht. + * vollstaendig, nurText}</code> und liefert <code>{html, angewandt, manuell, normen}</code> — bei + * <code>nurText</code> stattdessen <code>{text}</code> — oder <code>{fehler}</code> zurück. + * Geworfen wird nichts: Eine Ausnahme im Wasm hinterlässt auf der JS-Seite nur einen unlesbaren + * Stapel, also wird jeder Fehler als Text zurückgereicht. * * <p>Der Dateiinhalt wandert als Base64-Text über die Grenze, nicht als {@code Uint8Array}: Die * Umsetzung typisierter Felder nach {@code byte[]} ist in Web Image derzeit defekt („byteArrayHub @@ -71,6 +72,14 @@ public final class BrowserMain { var artikel = text(eingabe.get("artikel")); var vollstaendig = Boolean.TRUE.equals(wahrheitswert(eingabe.get("vollstaendig"))); + if (Boolean.TRUE.equals(wahrheitswert(eingabe.get("nurText")))) { + // Der Notausgang der Befehlszeile (--extract-only) steht auch hier offen: Wer einem Rest + // nachgehen will, muss sehen können, was das Erzeugnis gelesen hat. + antwort.set( + "text", JSString.of(Pipeline.extrahiereText(stamm, List.copyOf(patches), false))); + return antwort; + } + var ergebnis = Pipeline.erzeugeSynopse( stamm, @@ -132,7 +141,8 @@ public final class BrowserMain { return b; } if (wert instanceof JSValue v) { - return v.asBoolean(); + // Ein nicht gesetztes Feld kommt als „undefined“ herüber; asBoolean() bräche daran. + return "undefined".equals(v.typeof()) ? Boolean.FALSE : v.asBoolean(); } return Boolean.FALSE; } |
