Die fachlichen Inhalte liegen als Dateien unter content/ und werden beim Start eingelesen, geprueft und in den Speicher geladen. - Inhaltslader mit Zod-Schemata und Querverweispruefung - Fehlermeldungen nennen Datei und Zeile, gesammelt statt beim ersten Fehler geworfen; die Anwendung startet bei Fehlern nicht - npm run inhalte:pruefen prueft ohne Start - Handout-Ansicht mit festem Aufbau, Uebersicht nach Themenbloecken - Volltextsuche mit Gewichtung und Trefferausschnitt - Gelesen-Markierung je Handout - Glossar mit Suche und automatischer Verlinkung in den Handouts - Beispielinhalte: 4 Wochen, 12 Handouts, 24 Merkkarten, 4 Aufgaben, 4 Checklisten, 2 Lernzielkontrollen, 24 Glossareintraege - rahmenplan.yaml aus FIAusbV 2020 und KMK-Rahmenlehrplan, mit Fundstellen - gray-matter entfernt, YAML-Kopf wird selbst herausgeloest - npm run screenshots nimmt die Bilder fuer die Anleitung auf - 39 neue Tests, insgesamt 68 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
import { normalisiere, type Handout, type Inhalte } from './laden';
|
||
|
||
/**
|
||
* Volltextsuche über die Handouts.
|
||
*
|
||
* Bewusst kein Suchindex und keine Bibliothek: Bei 52 Handouts ist ein
|
||
* Durchlauf über alle Texte im Mikrosekundenbereich, und ein Suchindex wäre
|
||
* eine zweite Wahrheit, die man pflegen müsste.
|
||
*/
|
||
|
||
export type Fundstueck = {
|
||
handout: Handout;
|
||
punkte: number;
|
||
ausschnitt: string;
|
||
};
|
||
|
||
/** Zerlegt die Eingabe in Wörter; Anführungszeichen halten eine Wortgruppe zusammen. */
|
||
export function suchbegriffe(eingabe: string): string[] {
|
||
const treffer = eingabe.match(/"[^"]+"|\S+/g) ?? [];
|
||
return treffer.map((t) => normalisiere(t.replace(/^"|"$/g, ''))).filter((t) => t.length > 1);
|
||
}
|
||
|
||
export function sucheHandouts(alle: Inhalte, eingabe: string, grenze = 30): Fundstueck[] {
|
||
const begriffe = suchbegriffe(eingabe);
|
||
if (begriffe.length === 0) return [];
|
||
|
||
const gefunden: Fundstueck[] = [];
|
||
|
||
for (const handout of alle.handouts) {
|
||
const titel = normalisiere(`${handout.id} ${handout.titel}`);
|
||
const worum = normalisiere(handout.worum);
|
||
const merksatz = normalisiere(handout.merksatz);
|
||
|
||
let punkte = 0;
|
||
let alleEnthalten = true;
|
||
|
||
for (const begriff of begriffe) {
|
||
let treffer = 0;
|
||
if (titel.includes(begriff)) treffer += 10;
|
||
if (worum.includes(begriff)) treffer += 5;
|
||
if (merksatz.includes(begriff)) treffer += 4;
|
||
if (handout.suchtext.includes(begriff)) treffer += 1;
|
||
|
||
if (treffer === 0) {
|
||
alleEnthalten = false;
|
||
break;
|
||
}
|
||
punkte += treffer;
|
||
}
|
||
|
||
if (!alleEnthalten) continue;
|
||
|
||
gefunden.push({ handout, punkte, ausschnitt: ausschnittUm(handout, begriffe[0]!) });
|
||
}
|
||
|
||
return gefunden
|
||
.sort((a, b) => b.punkte - a.punkte || a.handout.id.localeCompare(b.handout.id, 'de-DE'))
|
||
.slice(0, grenze);
|
||
}
|
||
|
||
/**
|
||
* Ein Stück Text rund um den ersten Treffer, damit man in der Trefferliste
|
||
* sieht, warum etwas gefunden wurde.
|
||
*/
|
||
function ausschnittUm(handout: Handout, begriff: string, laenge = 160): string {
|
||
const stelle = handout.suchtext.indexOf(begriff);
|
||
if (stelle === -1) return handout.worum;
|
||
|
||
// Der normalisierte Text hat dieselbe Länge wie das Original, weil nur
|
||
// kleingeschrieben und Leerraum vereinheitlicht wird – bis auf Umlaute.
|
||
// Für einen Ausschnitt reicht das allemal.
|
||
const roh = `${handout.id} ${handout.titel} ${handout.worum} ${handout.merksatz} ${handout.rumpf}`
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
|
||
const mitte = Math.min(stelle, Math.max(0, roh.length - 1));
|
||
const von = Math.max(0, mitte - Math.floor(laenge / 3));
|
||
const bis = Math.min(roh.length, von + laenge);
|
||
|
||
const teil = roh.slice(von, bis).trim();
|
||
return `${von > 0 ? '… ' : ''}${teil}${bis < roh.length ? ' …' : ''}`;
|
||
}
|