waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src shared hervorhebung.ts

4,0 KB Rohdatei
app/src/shared/hervorhebung.ts — 117 Zeilen
1 /**
2 * Fundstellen im amtlichen Wortlaut markieren.
3 *
4 * ## Warum das ein eigenes Modul ist
5 *
6 * Weil hier zwei Koordinatensysteme aufeinandertreffen. Gesucht wird in der
7 * Kanonform („schiessstate“), angezeigt wird der Wortlaut („Schießstätte“) –
8 * und die Faltung ändert Längen. Aus „ß“ wird ein Zeichen, aus „ü“ eines,
9 * aus „tt“ eines. Wer die Stelle aus der Kanonform unbesehen auf den
10 * Ursprungstext anwendet, markiert daneben. Nachgemessen enthalten 91,7
11 * Prozent der Fragen Umlaute oder ß; danebenzumarkieren wäre also der
12 * Regelfall, nicht die Ausnahme.
13 *
14 * Eine falsch markierte Stelle ist schlimmer als gar keine: Sie behauptet,
15 * das gesuchte Wort stehe dort, wo es nicht steht. Deshalb liefert
16 * {@link spannenFinden} im Zweifel nichts, statt zu raten.
17 *
18 * ## Warum das ohne zweiten Parser geht
19 *
20 * Der Katalog führt jeden Textblock doppelt: als `text` und als `segmente`.
21 * Nachgemessen über alle 2.109 Blöcke ergibt das Aneinanderhängen der
22 * Segmente **exakt** den Text – ohne eine einzige Abweichung. Eine Spanne in
23 * Zeichen lässt sich damit verlustfrei auf die Segmente abbilden.
24 */
25
26 import type { RichText } from './katalog';
27 import { aufUrsprung, faltenMitZuordnung, fundstellen } from './suchtext';
28
29 /** Eine Fundstelle im Ursprungstext: `[von, bis)`. */
30 export interface Spanne {
31 readonly von: number;
32 readonly bis: number;
33 }
34
35 /**
36 * Alle Fundstellen der Suchwörter im Ursprungstext, zusammengefasst.
37 *
38 * Überlappende und aneinandergrenzende Spannen werden verschmolzen – sonst
39 * entstünden bei „waffe waffen“ zwei ineinanderliegende `<mark>`, und
40 * Bildschirmleser sagten die Hervorhebung doppelt an.
41 */
42 export function spannenFinden(roh: string, woerter: readonly string[]): Spanne[] {
43 if (woerter.length === 0 || roh.length === 0) {
44 return [];
45 }
46
47 const faltung = faltenMitZuordnung(roh);
48 const roh_spannen: Spanne[] = [];
49
50 for (const wort of woerter) {
51 if (wort.length === 0) {
52 continue;
53 }
54 for (const stelle of fundstellen(faltung.gefaltet, wort)) {
55 const spanne = aufUrsprung(faltung, stelle, stelle + wort.length);
56 if (spanne !== null) {
57 roh_spannen.push(spanne);
58 }
59 }
60 }
61
62 roh_spannen.sort((a, b) => a.von - b.von || a.bis - b.bis);
63
64 const aus: Spanne[] = [];
65 for (const spanne of roh_spannen) {
66 const letzte = aus[aus.length - 1];
67 if (letzte !== undefined && spanne.von <= letzte.bis) {
68 aus[aus.length - 1] = { von: letzte.von, bis: Math.max(letzte.bis, spanne.bis) };
69 continue;
70 }
71 aus.push(spanne);
72 }
73 return aus;
74 }
75
76 /** Ein Stück Text mit seinen beiden Auszeichnungen. */
77 export interface Stueck {
78 readonly t: string;
79 /** Auszeichnung des amtlichen Wortlauts (Verneinung, Kernelement). */
80 readonly hervorgehoben: boolean;
81 /** Fundstelle der Suche. */
82 readonly treffer: boolean;
83 }
84
85 /**
86 * Zerteilt einen Textblock so, dass beide Auszeichnungen nebeneinander
87 * bestehen können.
88 *
89 * Die amtliche Auszeichnung und die Fundstelle der Suche dürfen ineinander
90 * liegen, ohne einander zu überschreiben: Die eine gehört zum Wortlaut, die
91 * andere zur Suche. Sie zu vermischen hieße, dem Nutzer eine Auszeichnung zu
92 * zeigen, die im Katalog nicht steht.
93 */
94 export function zerteilen(text: RichText, spannen: readonly Spanne[]): Stueck[] {
95 const segmente = text.segmente.length > 0 ? text.segmente : [{ t: text.text }];
96
97 const stuecke: Stueck[] = [];
98 let stelle = 0;
99
100 const istTreffer = (i: number): boolean => spannen.some((s) => i >= s.von && i < s.bis);
101
102 for (const segment of segmente) {
103 for (const zeichen of segment.t) {
104 const treffer = istTreffer(stelle);
105 const hervorgehoben = segment.h === true;
106 const letztes = stuecke[stuecke.length - 1];
107 if (letztes?.treffer === treffer && letztes.hervorgehoben === hervorgehoben) {
108 stuecke[stuecke.length - 1] = { ...letztes, t: letztes.t + zeichen };
109 } else {
110 stuecke.push({ t: zeichen, hervorgehoben, treffer });
111 }
112 stelle += zeichen.length;
113 }
114 }
115
116 return stuecke;
117 }