Files
lernbar/src/lib/inhalte/suche.ts
T
Florian WildeandClaude Opus 5 1af5613e9b Baustein 2: Inhalte einlesen und pruefen, Handouts, Suche, Glossar
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>
2026-09-10 07:56:38 +02:00

83 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ? ' …' : ''}`;
}