From ec4bfc593908776ce0b8f396250cb95f12b4ec27 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 12 Oct 2026 19:55:30 +0200 Subject: [PATCH 1/6] docs: add German user manual (BEDIENUNGSANLEITUNG.md) Installation, query syntax, CLI, Python API, Web API, widget, server operation (gunicorn, systemd, nginx), security, recipes, error codes, troubleshooting and release workflow. Linked from README. Co-Authored-By: Claude Opus 5.5 --- BEDIENUNGSANLEITUNG.md | 746 +++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 748 insertions(+) create mode 100644 BEDIENUNGSANLEITUNG.md diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md new file mode 100644 index 0000000..04809c5 --- /dev/null +++ b/BEDIENUNGSANLEITUNG.md @@ -0,0 +1,746 @@ +# mdgrep – Bedienungsanleitung + +**mdgrep** ist eine Volltextsuche für Markdown-Dateien und beliebige andere Texte. Sie versteht +Wortbestandteile, Phrasen, reguläre Ausdrücke und logische Verknüpfungen mit Klammern – und lässt +sich auf vier Arten nutzen: + +| Baustein | Wofür | +|---|---| +| **Kommandozeile** `mdgrep` | schnell im Terminal suchen, wie `grep` – aber blockweise und mit UND/ODER/NICHT | +| **Python-Bibliothek** | Suche in eigene Programme einbauen, auch für Nicht-Markdown-Daten | +| **Web-API** (Flask) | JSON-Suchschnittstelle für Webseiten und andere Dienste | +| **Such-Widget** (JavaScript) | fertige Suchmaske mit Live-Ergebnissen für jede Webseite | + +Version: 0.1.1 · Lizenz: MIT · Repository: https://kitux.de/forgejo/dschlueter/mdgrep + +--- + +## Inhaltsverzeichnis + +1. [Installation](#1-installation) +2. [Suchsyntax](#2-suchsyntax) +3. [Kommandozeile](#3-kommandozeile) +4. [Python-Bibliothek](#4-python-bibliothek) +5. [Web-API](#5-web-api) +6. [Such-Widget](#6-such-widget) +7. [Betrieb auf einem Server](#7-betrieb-auf-einem-server) +8. [Rezepte](#8-rezepte) +9. [Fehlermeldungen](#9-fehlermeldungen) +10. [Fehlersuche](#10-fehlersuche) +11. [Entwicklung & neue Versionen](#11-entwicklung--neue-versionen) + +--- + +## 1. Installation + +Voraussetzung: Python 3.10 oder neuer, `git` (für die Installation aus dem Repository). + +```bash +# nur Bibliothek + Kommandozeile +pip install "mdgrep @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1" + +# zusätzlich Web-API (installiert Flask) +pip install "mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1" +``` + +In einer `requirements.txt`: + +``` +mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1 +``` + +Prüfen: + +```bash +mdgrep --version # mdgrep 0.1.1 +python -c "import mdgrep; print(mdgrep.__version__)" +``` + +> **Hinweis:** mdgrep ist nicht auf PyPI veröffentlicht. Immer mit Git-URL und Versions-Tag +> installieren – ein bloßes `pip install mdgrep` findet das Paket nicht (oder, falls jemand den +> Namen dort später belegt, ein fremdes). + +--- + +## 2. Suchsyntax + +Groß- und Kleinschreibung spielen **nie** eine Rolle (auch nicht bei Umlauten). + +| Eingabe | Bedeutung | Beispiel findet … | +|---|---|---| +| `rust` | Wortbestandteil | „Rust", „Rustacean", „trusted" | +| `"open source"` | genaue Phrase; beliebiger Leerraum (auch Zeilenumbruch) zwischen den Wörtern | „Open Source", „open
source" | +| `/llms?\b/` | regulärer Ausdruck | „LLM", „LLMs" | +| `a b` · `a AND b` · `a UND b` · `a & b` · `a && b` | beide Begriffe | | +| `a OR b` · `a ODER b` · `a \| b` · `a \|\| b` | mindestens einer | | +| `NOT a` · `NICHT a` · `-a` · `!a` | Begriff darf nicht vorkommen | | +| `( … )` | Klammern | | + +**Vorrang:** NICHT bindet stärker als UND, UND stärker als ODER. +`a OR b AND c` bedeutet also `a OR (b AND c)`. Im Zweifel Klammern setzen. + +**Hinweise:** + +- Operatoren werden nur in **Großbuchstaben** erkannt: `and` ist ein normales Suchwort, `AND` der Operator. +- Wortbestandteile treffen auch mitten im Wort: `go` findet „**Go**ddard". Für **ganze Wörter** + einen regulären Ausdruck mit Wortgrenzen verwenden: `/\bgo\b/`. +- Im regulären Ausdruck einen Schrägstrich als `\/` schreiben: `/tcp\/ip/`. +- In Phrasen ein Anführungszeichen als `\"` schreiben. +- Wörter mit Sonderzeichen funktionieren direkt: `AT&T`, `TCP/IP`, `e-mail` + (ein `-` oder `&` *innerhalb* eines Wortes ist kein Operator). + +**Beispiele:** + +```text +(rust ODER go) UND NICHT crypto +"machine learning" -/pytorch|tensorflow/ +llm (datenschutz OR privacy) +/\bgpt-\d+\b/ NICHT "open source" +``` + +**Grenzen (Standard):** höchstens 500 Zeichen und 50 Suchbegriffe pro Anfrage +(die Web-API begrenzt zusätzlich auf 300 Zeichen). + +--- + +## 3. Kommandozeile + +```bash +mdgrep SUCHANFRAGE [PFAD ...] [Optionen] +``` + +Ohne Pfad wird das aktuelle Verzeichnis durchsucht. Verzeichnisse werden rekursiv nach `*.md` +durchsucht. Die Suchanfrage immer in **einfache Anführungszeichen** setzen, damit die Shell +Klammern, `!`, `|` und `&` nicht selbst auswertet. + +### 3.1 Beispiele + +```bash +mdgrep '(rust OR go) AND NOT crypto' notizen/ # alle *.md unter notizen/ +mdgrep '/\bllms?\b/' docs/*.md --split list # ein Treffer pro Listenpunkt +mdgrep 'todo' . -l # nur Dateinamen +mdgrep 'release' . -c # nur Anzahl der Treffer +mdgrep '"open source"' . --json > treffer.json # JSON für andere Programme +mdgrep 'fehler' logs/ -g '*.txt' # andere Dateien als Markdown +mdgrep 'rust' . -m 5 # nach 5 Treffern aufhören +``` + +Ausgabe (Fundstellen farbig, wenn die Ausgabe ein Terminal ist): + +```text +notizen/sprachen.md:5 + Rust guarantees memory safety without a garbage collector. Its borrow checker + rejects data races at compile time. +``` + +### 3.2 Optionen + +| Option | Bedeutung | Standard | +|---|---|---| +| `-s`, `--split MODUS` | Einheit, die durchsucht und angezeigt wird (siehe 3.3) | `paragraph` | +| `-g`, `--glob MUSTER` | Dateimuster in Verzeichnissen | `*.md` | +| `-c`, `--count` | nur Anzahl der Treffer-Blöcke ausgeben | | +| `-l`, `--files-with-matches` | nur Dateinamen ausgeben | | +| `-j`, `--json` | JSON-Ausgabe (gleiches Format wie die Web-API) | | +| `-C`, `--context N` | Zeichen Kontext um Treffer bei langen Blöcken | `80` | +| `--max-chars N` | Blöcke über N Zeichen kürzen (`0` = nie) | `400` | +| `-m`, `--max-count N` | nach N Treffern aufhören (`0` = alle) | `0` | +| `--timeout SEK` | Zeitlimit für die ganze Suche | `30` | +| `--color auto\|always\|never` | Farbe (`NO_COLOR` wird beachtet) | `auto` | +| `-V`, `--version` | Version anzeigen | | + +### 3.3 Split-Modi – was ist ein Treffer? + +| Modus | Ein Block ist … | Geeignet für | +|---|---|---| +| `paragraph` | ein durch Leerzeilen getrennter Absatz | Fließtext, Notizen | +| `list` | ein Listenpunkt der obersten Ebene (mit eingerückten Unterpunkten); Text außerhalb von Listen wird zu Absätzen | Aufzählungen, Berichte, Changelogs | +| `heading` | ein Abschnitt von Überschrift zu Überschrift | Dokumentation, Wikis | +| `file` | eine ganze Datei | „In welchen Dateien kommt X und Y vor?" | + +Die logische Verknüpfung gilt **pro Block**: `rust AND go` findet nur Blöcke, in denen beide +Wörter vorkommen. Mit `--split file` gilt sie für die ganze Datei. + +### 3.4 Rückgabewerte (für Skripte) + +| Code | Bedeutung | +|---|---| +| `0` | Treffer gefunden | +| `1` | kein Treffer | +| `2` | Fehler: Syntaxfehler (mit Markierung der Position), Datei nicht lesbar oder Zeitlimit erreicht | + +```bash +if mdgrep 'TODO OR FIXME' src/ -c > /dev/null; then echo "Es gibt noch offene Punkte"; fi +``` + +--- + +## 4. Python-Bibliothek + +### 4.1 Nur die Suchsprache + +```python +from mdgrep import compile_query, QuerySyntaxError + +q = compile_query('(rust OR go) AND NOT /crypto(currency)?/') +q.matches("Rust 2.0 is out") # True +q.spans("Rust and Go") # [(0, 4), (9, 11)] → Positionen zum Hervorheben + +try: + compile_query("(rust OR go") +except QuerySyntaxError as e: + print(e.code, e.position, e.message) # unclosed_paren 0 Missing closing parenthesis +``` + +- `compile_query(text, max_length=500, max_terms=50)` → `Query` +- `Query.matches(text, timeout=None)` → `bool` +- `Query.spans(text, timeout=None)` → Liste von `(start, ende)`, sortiert und zusammengefasst; + Begriffe unter NICHT werden nicht markiert +- Bei Überschreitung von `timeout` (Sekunden) wird `QueryTimeout` ausgelöst + +### 4.2 Markdown-Dateien durchsuchen + +```python +from mdgrep import load_markdown, SearchEngine + +blocks = load_markdown("docs/", split="list", relative_to="docs/") +engine = SearchEngine(blocks) + +result = engine.search("rust -crypto", limit=20) +print(result.total_blocks, "Treffer") +for group in result.groups: + b = group.block + print(f"{b.source}:{b.line}", "".join(s["t"] for s in group.segments)) +``` + +`load_markdown(pfade, ...)`: + +| Parameter | Bedeutung | Standard | +|---|---|---| +| `paths` | Datei, Verzeichnis oder Liste davon | | +| `split` | `file` · `heading` · `list` · `paragraph` | `paragraph` | +| `meta` | Funktion `pfad → dict`; wird in die Metadaten jedes Blocks der Datei übernommen | | +| `relative_to` | Verzeichnis, relativ zu dem `source`/`id` angegeben werden (wichtig für Web: keine Serverpfade preisgeben) | absolute/angegebene Pfade | +| `pattern` | Dateimuster in Verzeichnissen | `*.md` | +| `dialect` | `commonmark` oder `python-markdown` (siehe 4.6) | `commonmark` | +| `encoding` | Zeichenkodierung | `utf-8` | + +Jeder Block bekommt in `meta` automatisch: `section` (nächste Überschrift darüber), +`kind` (`file`/`section`/`item`/`paragraph`) und `markdown` (Original-Quelltext). +Durchsucht wird der Text **ohne** Markdown-Zeichen (Links, Fett, Code …). + +### 4.3 Eigene Daten durchsuchen + +mdgrep ist nicht auf Markdown beschränkt. Jede Datenquelle lässt sich als Liste von `Block`-Objekten +übergeben: + +```python +from mdgrep import Block, SearchEngine + +tickets = [ + Block(id="42", title="Ticket 42", text="Login schlägt mit SSO fehl", meta={"status": "offen"}), + Block(id="43", title="Ticket 43", text="Dunkelmodus-Schalter defekt", meta={"status": "erledigt"}), +] +engine = SearchEngine(tickets) +engine.search("login ODER sso", filters={"status": "offen"}) +``` + +| Feld | Bedeutung | +|---|---| +| `id` | eindeutige Kennung | +| `title` | wird durchsucht und als Überschrift angezeigt (nicht im `text` wiederholen) | +| `text` | wird durchsucht und als Ausschnitt angezeigt | +| `anchor` | optionale Sprungmarke (`#anchor`), Linkziel im Widget, wenn kein `href` gesetzt ist | +| `source`, `line` | Herkunft (Datei, Zeile ab 1) | +| `meta` | beliebiges Wörterbuch; wird unverändert durchgereicht, Filter arbeiten darauf | + +### 4.4 Suchoptionen + +```python +result = engine.search( + "rust", + filters={"lang": ["de", "en"]}, # alle Schlüssel müssen passen; leere Werte = egal + predicate=lambda b: b.line > 1, # zusätzlicher eigener Filter + group_by=lambda b: b.meta.get("url"), # gleiche Schlüssel → ein Ergebnis, Rest in group.also + sort_key=lambda b: b.meta["date"], reverse=True, # neueste zuerst + offset=0, limit=30, # Seiten (zählt Gruppen) + time_budget=1.5, # Sekunden; danach Teilergebnis mit truncated=True + snippet_chars=700, context=120, # lange Texte auf Fenster um die Treffer kürzen +) +``` + +Das Ergebnis (`SearchResult`): + +| Attribut | Bedeutung | +|---|---| +| `groups` | Treffer der aktuellen Seite; je Gruppe `block`, `segments`, `title_segments`, `hits`, `also` | +| `total_groups`, `total_blocks` | Anzahl Ergebnisse bzw. Fundstellen insgesamt | +| `next_offset` | `offset` der nächsten Seite oder `None` | +| `truncated` | `True`, wenn das Zeitbudget aufgebraucht wurde | +| `elapsed_ms` | Suchdauer | +| `to_dict()` | JSON-fähiges Wörterbuch (Format siehe 5.3) | + +`segments` ist eine Liste `[{"t": "Text", "hit": False}, {"t": "Treffer", "hit": True}, …]` – +fertig zum Anzeigen mit Hervorhebung. + +### 4.5 Automatisch neu laden + +Ändern sich die Dateien, lädt die Suchmaschine sie selbst neu – ohne Neustart: + +```python +from pathlib import Path + +def stand(verzeichnis=Path("docs")): + dateien = [p.stat() for p in verzeichnis.rglob("*.md")] + return len(dateien), max((s.st_mtime for s in dateien), default=0) + +engine = SearchEngine(lambda: load_markdown("docs", relative_to="docs"), + reload_check=stand, reload_interval=60) +``` + +`reload_check` liefert einen beliebigen vergleichbaren Wert; ändert er sich, wird neu geladen. +Geprüft wird höchstens alle `reload_interval` Sekunden. `engine.reload()` lädt sofort neu. + +### 4.6 Markdown-Dialekt – wenn Treffer auf HTML-Elemente verlinken + +Wer Treffer mit Sprungmarken in gerenderten HTML-Seiten verknüpft, braucht dieselbe Aufteilung wie +der Markdown-Renderer. Die Renderer unterscheiden sich bei Listen: + +| | `commonmark` (Standard) | `python-markdown` | +|---|---|---| +| Liste direkt unter einer Absatzzeile (ohne Leerzeile) | beginnt eine Liste | gehört zum Absatz | +| Unterpunkt mit 1–3 Leerzeichen Einrückung | verschachtelt | eigener Punkt auf oberster Ebene | + +```python +split_markdown(text, split="list", dialect="python-markdown") +``` + +### 4.7 Weitere Funktionen + +| Funktion | Zweck | +|---|---| +| `split_markdown(text, source=…, split=…, meta=…, dialect=…)` | einen Markdown-Text (statt Dateien) zerlegen | +| `markdown_to_text(md)` | Markdown-Zeichen entfernen, Zeilen erhalten | +| `segments(text, spans, max_chars=…, context=…)` | Text + Fundstellen → Anzeige-Segmente | +| `export_static(zielverzeichnis)` | Widget-Dateien (JS + CSS) kopieren, siehe 6.4 | + +--- + +## 5. Web-API + +Benötigt `mdgrep[web]`. + +### 5.1 Eigenständige App + +```python +# app.py +from mdgrep import SearchEngine, load_markdown +from mdgrep.web import create_app + +engine = SearchEngine(lambda: load_markdown("docs", split="list", relative_to="docs")) +app = create_app(engine, filters=["kind"]) # GET /search?q=…&kind=… +``` + +### 5.2 In eine bestehende Flask-App einhängen + +```python +from mdgrep.web import create_blueprint + +app.register_blueprint(create_blueprint(engine, filters=["lang"]), url_prefix="/api") +# → GET /api/search?q=… und Widget-Dateien unter /api/mdgrep-static/ +``` + +Optionen von `create_blueprint()` (zusätzlich zu `group_by`, `sort_key`, `reverse`, +`time_budget`, `snippet_chars`, `context` aus 4.4): + +| Option | Bedeutung | Standard | +|---|---|---| +| `filters` | Metadaten-Schlüssel, die als URL-Parameter filterbar sind | keine | +| `default_limit` / `max_limit` | Ergebnisse pro Seite / Obergrenze | `30` / `100` | +| `max_query_length` | maximale Länge der Suchanfrage | `300` | +| `hide_meta` | Metadaten-Schlüssel, die nicht ausgeliefert werden | `("markdown",)` | +| `name` | Blueprint-Name (bei mehreren Suchen in einer App) | `mdgrep` | + +`create_app(engine, url_prefix="", static_dir=None, **optionen)` – mit `static_dir` liefert die App +zusätzlich die Dateien dieses Verzeichnisses unter `/` aus (praktisch zum Testen einer statischen Seite). + +### 5.3 Anfragen und Antworten + +```text +GET /search?q=rust%20-crypto&lang=de&offset=0&limit=30 +``` + +- `q` – Suchanfrage (leer → leeres Ergebnis) +- je Name in `filters` ein Parameter: leer oder `all` = keine Einschränkung, `de,en` = mehrere Werte +- `offset`, `limit` – Seiten + +Antwort (HTTP 200): + +```json +{ + "query": "rust -crypto", "total_groups": 12, "total_blocks": 15, + "offset": 0, "limit": 30, "next_offset": null, "truncated": false, "elapsed_ms": 3.2, + "results": [{ + "block": { + "id": "a.md:4", "title": "…", "anchor": "", "source": "a.md", "line": 4, "meta": {}, + "title_segments": [{"t": "…", "hit": false}], + "segments": [{"t": "The ", "hit": false}, {"t": "Rust", "hit": true}, {"t": " team …", "hit": false}], + "hits": 1 + }, + "also": [{"id": "b.md:9", "title": "…", "anchor": "", "source": "b.md", "line": 9, "meta": {}}] + }] +} +``` + +Fehler (HTTP 400): + +```json +{"error": {"code": "unclosed_paren", "message": "Missing closing parenthesis", "position": 0}} +``` + +### 5.4 Entwicklungsserver + +```bash +python -m mdgrep.web --dir docs --split list --static site --port 8000 +# → http://127.0.0.1:8000/ (Dateien aus site/) +# → http://127.0.0.1:8000/api/search (API) +``` + +| Option | Bedeutung | Standard | +|---|---|---| +| `--dir` | Verzeichnis mit Markdown-Dateien (Pflicht) | | +| `--split` | Split-Modus | `paragraph` | +| `--static` | dieses Verzeichnis unter `/` ausliefern; API dann unter `--api-prefix` | | +| `--api-prefix` | Pfad der API, wenn `--static` gesetzt ist | `/api` | +| `--filters` | kommagetrennte Metadaten-Schlüssel als Filter, z. B. `kind,section` | | +| `--host`, `--port` | Adresse | `127.0.0.1`, `8000` | + +Neue oder geänderte Dateien werden im Entwicklungsserver nach spätestens 5 Sekunden übernommen. +Ein vollständiges Beispiel liegt in `examples/`: + +```bash +python -m mdgrep.web --dir examples/docs --split list --static examples/site +``` + +--- + +## 6. Such-Widget + +Eine fertige Suchmaske: Ergebnisse erscheinen beim Tippen, veraltete Anfragen werden abgebrochen, +die Suche steht in der URL (teilbar, Zurück-Taste funktioniert), Filter, „Mehr laden", +aufklappbare Syntaxhilfe, Fehlermeldungen mit Position, Beschriftung Deutsch oder Englisch. +Alle Texte werden sicher als Text eingefügt (kein HTML vom Server → kein Cross-Site-Scripting). +Keine Abhängigkeiten, kein Framework. + +### 6.1 Einbinden + +```html + +
+ + +``` + +Ganz ohne eigenes JavaScript: + +```html +
+``` + +### 6.2 Optionen + +| Option | Bedeutung | Standard | +|---|---|---| +| `endpoint` | URL der Such-API (relativ oder absolut) | `search` | +| `labels` | `'de'`, `'en'` oder Objekt mit einzelnen Texten (siehe 6.5) | `'en'` | +| `filters` | Auswahlfelder `[{name, label, options:[{value,label}], value}]`; `name` = URL-Parameter der API | `[]` | +| `live` | beim Tippen suchen | `true` | +| `debounce` | Wartezeit nach dem letzten Tastendruck (ms) | `300` | +| `minChars` | Mindestlänge der Eingabe | `2` | +| `limit` | Ergebnisse pro Seite | `30` | +| `syncUrl` | Suche und Filter in der Seiten-URL halten | `true` | +| `autofocus` | Eingabefeld beim Laden fokussieren | `true` | +| `help` | Syntaxhilfe anzeigen | `true` | +| `hrefField` | Metadaten-Feld mit dem Linkziel | `href` | +| `href(block)` | eigene Funktion für das Linkziel (hat Vorrang) | | +| `dateField` | Metadaten-Feld, das als Datum angezeigt wird | | +| `badgeFields` | Metadaten-Felder, die als Plaketten angezeigt werden | `[]` | +| `formatBadge(feld, wert, block)` | Text einer Plakette anpassen | | +| `alsoText(block)` | Linktext in der Liste „auch in …" | Datum bzw. Titel | + +Ohne `href` und ohne `hrefField`-Wert verlinkt ein Treffer auf `#anchor` des Blocks. + +### 6.3 Steuern per JavaScript + +```javascript +var suche = MdGrep.mount(element, optionen); +suche.setQuery('rust -crypto'); // Suchbegriff setzen und suchen +suche.search(); // erneut suchen +suche.input; // das Eingabefeld + +element.addEventListener('mdgrep:results', function (e) { + console.log(e.detail.total_groups); // vollständige API-Antwort +}); +``` + +Ist die Seite mit `?q=…` aufgerufen worden, sucht das Widget sofort. So kann ein einfaches +Formular auf einer anderen Seite zur Suchseite weiterleiten: + +```html +
+``` + +### 6.4 Statische Webseiten + +Liegt die Seite nicht in Flask, die Widget-Dateien einmalig (oder bei jedem Build) kopieren: + +```python +import mdgrep +mdgrep.export_static("site/static") # schreibt mdgrep-widget.js und mdgrep-widget.css +``` + +Die Dateien werden nur neu geschrieben, wenn sich ihr Inhalt geändert hat. + +### 6.5 Aussehen anpassen + +Farben und Rundungen über CSS-Variablen (an `:root` oder einem umgebenden Element): + +```css +:root { + --mdgrep-text: #1a1d2e; /* Text */ + --mdgrep-muted: #6b7394; /* Nebentexte */ + --mdgrep-surface: #ffffff; /* Kästchen, Eingabefeld */ + --mdgrep-border: #d5d8e3; /* Rahmen */ + --mdgrep-accent: #4a7cff; /* Fokus, Hover */ + --mdgrep-mark-bg: #ffe58a; /* Hervorhebung */ + --mdgrep-mark-text: inherit; + --mdgrep-error: #c0392b; /* Fehlermeldungen */ + --mdgrep-radius: 0.5rem; +} +``` + +Für einen Dunkelmodus die Variablen auf die eigenen Farbvariablen abbilden, z. B. +`--mdgrep-surface: var(--meine-flaeche);` – dann folgt das Widget dem Seitenthema automatisch. + +Einzelne Texte überschreiben: + +```javascript +labels: { lang: 'de', none: 'Leider nichts gefunden.', more: 'Weitere Treffer' } +``` + +--- + +## 7. Betrieb auf einem Server + +Der Flask-Entwicklungsserver ist nur zum Testen gedacht. Für den Dauerbetrieb: + +### 7.1 Mit gunicorn + +```bash +pip install gunicorn +gunicorn --workers 2 --bind 127.0.0.1:8790 app:app +``` + +Jeder Worker hält alle Blöcke im Speicher (Richtwert: ca. 90 MB für 15 MB Markdown / 26 000 Blöcke). + +### 7.2 Als systemd-Dienst + +```ini +# /etc/systemd/system/meine-suche.service +[Unit] +Description=Volltextsuche (mdgrep) +After=network.target + +[Service] +User=meinuser +WorkingDirectory=/home/meinuser/projekt +ExecStart=/home/meinuser/projekt/venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8790 app:app +Restart=on-failure +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload && sudo systemctl enable --now meine-suche +systemctl status meine-suche +journalctl -u meine-suche -n 50 +``` + +### 7.3 Hinter nginx mit Ratenbegrenzung + +```nginx +# http-Kontext, z. B. /etc/nginx/conf.d/suche.conf +limit_req_zone $binary_remote_addr zone=suche:10m rate=5r/s; + +# im server-Block +location = /api/search { + limit_req zone=suche burst=20 nodelay; + limit_req_status 429; + proxy_pass http://127.0.0.1:8790/search$is_args$args; + proxy_read_timeout 10s; +} +``` + +Das Widget zeigt bei HTTP 429 „Zu viele Anfragen – bitte einen Moment warten." + +### 7.4 Sicherheit + +- **Bösartige reguläre Ausdrücke** (z. B. `/(a|aa)+$/`) können exponentiell lange rechnen. mdgrep + begrenzt jede Suche auf `time_budget` (Web-API: 1,5 s) und liefert danach ein Teilergebnis + mit `truncated: true`. +- **Ratenbegrenzung** im Reverse-Proxy (7.3) verhindert, dass viele Anfragen den Server auslasten. +- **Keine Serverpfade** preisgeben: `load_markdown(..., relative_to=…)` verwenden. +- **Interne Metadaten** mit `hide_meta` aus Antworten entfernen. +- Das Widget fügt Ergebnisse nur als Text ein – auch Markdown mit eingebettetem HTML ist sicher. + +### 7.5 Leistung + +Ohne Index werden alle Blöcke bei jeder Suche durchlaufen. Richtwerte für 26 000 Blöcke (15 MB): +einfache Begriffe und Phrasen 30–70 ms, reguläre Ausdrücke und Verknüpfungen 100–150 ms. +Für deutlich größere Datenmengen (Hunderte MB) ist ein Index-basiertes Werkzeug besser geeignet. + +--- + +## 8. Rezepte + +**Notizen nach zwei Begriffen im selben Absatz durchsuchen** + +```bash +mdgrep 'kubernetes "persistent volume"' ~/notizen +``` + +**Dateien finden, die A enthalten, aber nicht B** + +```bash +mdgrep 'docker -podman' ~/wiki --split file -l +``` + +**Changelog: alle Einträge zu Sicherheitslücken** + +```bash +mdgrep '/\bCVE-\d{4}-\d+\b/ OR security' CHANGELOG.md --split list +``` + +**Treffer in einem Skript weiterverarbeiten** + +```bash +mdgrep 'rust' docs --json | python -c "import json,sys; print([r['block']['source'] for r in json.load(sys.stdin)['results']])" +``` + +**Suche mit Sprachfilter aus dem Dateinamen** (`bericht_de.md`, `bericht_en.md`) + +```python +engine = SearchEngine(load_markdown("berichte", split="list", relative_to="berichte", + meta=lambda p: {"lang": p.stem.rsplit("_", 1)[-1]})) +engine.search("datenschutz", filters={"lang": "de"}) +``` + +**Gleiche Einträge aus mehreren Dateien zusammenfassen, neueste zuerst** + +```python +engine.search("rust", group_by=lambda b: b.meta.get("url"), + sort_key=lambda b: b.meta["date"], reverse=True) +``` + +**Praxisbeispiel:** Die Hacker-News-Berichte auf https://jamulix.de/HackerNews/search.html nutzen +mdgrep mit einem kleinen Adapter (`hnews_search.py` im Projekt *hackernews*): Stories und Insights +als Blöcke, Sprache/Datum/Linkziel als Metadaten, Gruppierung gleicher Stories, Sprungmarken in den +HTML-Seiten (`dialect="python-markdown"`). + +--- + +## 9. Fehlermeldungen + +Ungültige Anfragen liefern einen festen Code (für eigene Übersetzungen), eine Meldung und die +Position (0-basiert; das Widget zeigt sie 1-basiert an). + +| Code | Bedeutung | Beispiel | +|---|---|---| +| `empty_query` | leere Anfrage | ` ` | +| `empty_phrase` | leere Phrase | `""` | +| `unterminated_phrase` | schließendes Anführungszeichen fehlt | `"open source` | +| `unterminated_regex` | schließender Schrägstrich fehlt | `/abc` | +| `invalid_regex` | regulärer Ausdruck ungültig | `/a(b/` | +| `missing_operand` | Suchbegriff erwartet | `rust OR` · `AND go` | +| `unexpected_paren` | schließende Klammer ohne öffnende | `rust)` | +| `unexpected_token` | Operator an unerwarteter Stelle | | +| `unclosed_paren` | schließende Klammer fehlt | `(rust OR go` | +| `empty_group` | leere Klammern | `()` | +| `too_long` | Anfrage zu lang | | +| `too_many_terms` | zu viele Suchbegriffe | | + +Auf der Kommandozeile wird die Stelle markiert: + +```text +mdgrep: Missing closing parenthesis + (rust OR go + ^ +``` + +--- + +## 10. Fehlersuche + +| Problem | Ursache / Lösung | +|---|---| +| `bash: !…: event not found` | Suchanfrage in **einfache** Anführungszeichen setzen: `mdgrep 'rust !crypto'` | +| `go` findet zu viel | Wortbestandteile treffen auch in „Goddard" – ganze Wörter mit `/\bgo\b/` | +| `and`/`or` wirken nicht als Operator | Operatoren nur in Großbuchstaben: `AND`, `OR`, `NOT` (oder `UND`, `ODER`, `NICHT`) | +| Keine Treffer in `.txt`-Dateien | Standardmuster ist `*.md` → `-g '*.txt'` bzw. `pattern="*.txt"` | +| `ModuleNotFoundError: flask` | Web-Teil braucht `pip install "mdgrep[web] @ git+…"` | +| Widget zeigt „Die Suche ist fehlgeschlagen" | API nicht erreichbar: `endpoint` prüfen, Browser-Konsole/Netzwerk ansehen | +| Widget zeigt „Zu viele Anfragen" | Ratenbegrenzung des Proxys greift (HTTP 429) – kurz warten oder `burst` erhöhen | +| „Zeitlimit erreicht" | sehr aufwendiger regulärer Ausdruck; vereinfachen oder `time_budget` erhöhen | +| Neue Dateien werden nicht gefunden | `reload_check` gesetzt? Geprüft wird nur alle `reload_interval` Sekunden | +| Sprungmarken zeigen auf falsche Stellen | gleichen Markdown-`dialect` wie der HTML-Renderer verwenden (4.6) | +| Widget-Darstellung „verbogen" | Seiten-CSS überschreibt Elemente (z. B. `details`, `ol`, `button`); gezielt unter `.mdgrep …` zurücksetzen | + +--- + +## 11. Entwicklung & neue Versionen + +```bash +git clone https://kitux.de/forgejo/dschlueter/mdgrep.git && cd mdgrep +python -m venv .venv && .venv/bin/pip install -e ".[dev]" +.venv/bin/pytest # alle Tests +``` + +Aufbau: + +``` +src/mdgrep/ +├── query.py # Suchsprache: Tokenizer, Parser, Auswertung, Fundstellen +├── engine.py # Block, SearchEngine, SearchResult +├── snippets.py # Anzeige-Segmente, Kürzen um Treffer +├── markdown.py # Markdown → Blöcke, Markdown → Text +├── cli.py # Kommandozeile +├── web.py # Flask-Blueprint/-App, Entwicklungsserver +└── static/ # mdgrep-widget.js, mdgrep-widget.css +``` + +Neue Version veröffentlichen: + +1. Version in `pyproject.toml` **und** `src/mdgrep/__init__.py` erhöhen (bei Widget-Änderungen auch im Kopf von `mdgrep-widget.js`) +2. Tests laufen lassen: `.venv/bin/pytest` +3. Commit, Tag und Push: `git tag -a v0.1.2 -m "mdgrep 0.1.2" && git push origin main --tags` +4. In den nutzenden Projekten den Tag in `requirements.txt` anpassen und `pip install -r requirements.txt` + ausführen; laufende Dienste neu starten. + +Versionsregel: Änderungen an öffentlichen Funktionen, am JSON-Format oder an Widget-Optionen, die +bestehende Nutzer brechen, nur mit erhöhter Nebenversion (0.**2**.0) und Hinweis im Commit. diff --git a/README.md b/README.md index cdfacc5..377767f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Boolean full-text search over Markdown (and any other text) – as a **Python li - protection against catastrophic regular expressions (time limit per search) - no search index: blocks live in memory, a few MB of text are searched in milliseconds +Ausführliche deutsche Anleitung: [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md) + ## Installation ```bash From d9e495d36a88806107182cc08d0d0455b0d79669 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 12 Oct 2026 20:56:43 +0200 Subject: [PATCH 2/6] docs: add "Voraussetzungen an die Dokumente" to the manual New section 8: requirements for searching a collection (encoding, size, split mode vs. structure, metadata via naming scheme, limits) and for jump anchors (predictable HTML paths, three ways to ids, same Markdown dialect, keeping source and HTML in sync, consistency check script), plus a checklist. Following sections renumbered 9-12. Co-Authored-By: Claude Opus 5.5 --- BEDIENUNGSANLEITUNG.md | 119 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 04809c5..720931c 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -24,10 +24,11 @@ Version: 0.1.1 · Lizenz: MIT · Repository: https://kitux.de/forgejo/dschlueter 5. [Web-API](#5-web-api) 6. [Such-Widget](#6-such-widget) 7. [Betrieb auf einem Server](#7-betrieb-auf-einem-server) -8. [Rezepte](#8-rezepte) -9. [Fehlermeldungen](#9-fehlermeldungen) -10. [Fehlersuche](#10-fehlersuche) -11. [Entwicklung & neue Versionen](#11-entwicklung--neue-versionen) +8. [Voraussetzungen an die Dokumente](#8-voraussetzungen-an-die-dokumente) +9. [Rezepte](#9-rezepte) +10. [Fehlermeldungen](#10-fehlermeldungen) +11. [Fehlersuche](#11-fehlersuche) +12. [Entwicklung & neue Versionen](#12-entwicklung--neue-versionen) --- @@ -617,7 +618,109 @@ Für deutlich größere Datenmengen (Hunderte MB) ist ein Index-basiertes Werkze --- -## 8. Rezepte +## 8. Voraussetzungen an die Dokumente + +Welche Bedingungen eine Sammlung (z. B. Markdown-Dateien) erfüllen sollte – getrennt nach +„sinnvoll durchsuchen" und „Treffer springen direkt an die Fundstelle". + +### 8.1 Für die Suche + +1. **Textdateien mit bekannter Kodierung** – Standard UTF-8; sonst `encoding=`. Andere Endungen + als `.md` über `-g '*.txt'` bzw. `pattern=`. + +2. **Größe passend zum Ansatz** – mdgrep arbeitet ohne Index: alle Blöcke liegen im Speicher und + werden bei jeder Suche durchlaufen. Bis etwa 50 MB Text unproblematisch (Richtwert: 15 MB → + 30–150 ms pro Suche); bei Hunderten MB ist ein Index-basiertes Werkzeug besser. + +3. **Gliederung, die zu einem Split-Modus passt** – die UND/ODER/NICHT-Logik gilt **pro Block**, + die Blockgröße bestimmt also die Trefferqualität: + + | Aufbau der Dokumente | Split-Modus | + |---|---| + | Absätze, durch Leerzeilen getrennt | `paragraph` | + | Einträge als Listenpunkte (Berichte, Changelogs, Protokolle) | `list` | + | Abschnitte mit Überschriften (Dokumentation, Wiki) | `heading` | + | kurze Einzeldokumente | `file` | + + Zu große Blöcke machen `a AND b` beliebig (in einer langen Datei steht fast alles), zu kleine + verfehlen Begriffe, die in benachbarten Sätzen stehen. + +4. **Ableitbare Metadaten** – für Filter (Sprache, Kategorie), Sortierung (Datum) und Gruppierung + (gleicher Eintrag in mehreren Dateien) braucht jeder Block Werte in `meta`: + - am einfachsten über ein **einheitliches Datei- oder Pfad-Schema** + (z. B. `2026_10_12_bericht_de.md`) und `meta=lambda pfad: {…}`; + - YAML-Frontmatter wird **nicht** ausgewertet, sondern als Text durchsucht – dafür einen kleinen + Adapter schreiben, der Frontmatter liest und `Block`-Objekte erzeugt (siehe 4.3); + - zum Zusammenfassen von Duplikaten (`group_by`) einen **stabilen Schlüssel** pro Eintrag + (URL, ID). + +5. **Grenzen einplanen** + - **Keine Wortstammsuche:** „Haus" findet nicht „Häuser" → `/h(au|äu)s/` oder `haus ODER häuser`. + - **Keine Relevanz-Sortierung:** die Reihenfolge bestimmt `sort_key` (z. B. Datum). + - Markdown-Zeichen (Links, Fett, Code) werden vor der Suche entfernt; **direkt eingebettetes + HTML** (`
`) und Tabellen-Striche bleiben als Text stehen. + +### 8.2 Für Sprungmarken + +Damit ein Treffer direkt an die Fundstelle einer gerenderten Seite springt, müssen **zwei Seiten +exakt zusammenpassen**: die Blöcke von mdgrep und die IDs im HTML. + +1. **Gerenderte Seiten mit vorhersagbarer URL** – aus jeder Quelldatei muss sich ihr HTML-Pfad + berechnen lassen (`docs/a/b.md` → `site/a/b.html`). Diesen Pfad mit Sprungmarke in + `meta["href"]` schreiben (das Widget verlinkt darauf, siehe 6.2). + +2. **Jeder Block hat eine ID, die im HTML existiert** – drei Wege: + + | Weg | Voraussetzung | Bewertung | + |---|---|---| + | **Überschriften** (`split="heading"`) | Der Renderer erzeugt Überschriften-IDs mit **demselben** Verfahren wie mdgrep (`slugify`, GitHub-Stil) | wenig Aufwand, aber prüfen: Python-Markdown (`toc`), MkDocs, Hugo und GitHub behandeln Umlaute, Sonderzeichen und doppelte Überschriften unterschiedlich | + | **Listenpunkte/Absätze** (`split="list"` / `paragraph`) | Eigenes Nachbearbeitungs-Skript nummeriert die HTML-Elemente nach **derselben Reihenfolge-Regel** wie die Blöcke und setzt `block.anchor` entsprechend | mittlerer Aufwand, robust | + | **Text Fragments** (`seite.html#:~:text=…`) | Aktueller Browser; keine Änderung am HTML. Im Widget über die Option `href(block)` bauen | kein Aufwand, aber unzuverlässig bei mehrdeutigem oder umformatiertem Text | + + > **Bekannte Einschränkung:** `slugify` hängt bei **doppelten** Überschriften kein `-1`, `-2` an + > (anders als GitHub). Kommen Überschriften mehrfach vor, springen Treffer zur ersten. + +3. **Gleicher Markdown-Dialekt wie der Renderer** – Renderer zerlegen Listen unterschiedlich + (siehe 4.6). Passt `dialect` nicht, verschiebt sich die Nummerierung und Links zeigen auf falsche + Absätze. + +4. **Quelle und HTML auf demselben Stand** – nach jeder Änderung einer Quelldatei deren Seite neu + erzeugen; ändert sich die Zählregel, **alle** Seiten. + +5. **Einmal vollständig prüfen** – für jeden Block muss die Sprungmarke im HTML existieren **und** + auf ein Element zeigen, das seinen Text enthält. Skizze: + + ```python + import re + from bs4 import BeautifulSoup + + def pruefe(blocks, html_von): # html_von(block) → gerendertes HTML der Seite + fehler = [] + for b in blocks: + el = BeautifulSoup(html_von(b), "html.parser").find(id=b.anchor) + probe = re.sub(r"\s+", " ", (b.title or b.text))[:40].lower() + if el is None or probe not in re.sub(r"\s+", " ", el.get_text()).lower(): + fehler.append((b.source, b.anchor)) + return fehler + ``` + + Das deckt Formatvarianten älterer Dokumente auf, die man sonst übersieht. + +### 8.3 Checkliste + +| | Bedingung | +|---|---| +| ☐ | Textdateien, bekannte Kodierung, insgesamt höchstens im zweistelligen MB-Bereich | +| ☐ | Gliederung passt zu einem Split-Modus; Blöcke sind sinnvolle Einheiten | +| ☐ | Metadaten über einheitliches Datei-Namensschema (oder Adapter) | +| ☐ | stabiler Schlüssel pro Eintrag, falls Duplikate zusammengefasst werden | +| ☐ | *Sprungmarken:* HTML-Pfad aus Quellpfad berechenbar | +| ☐ | *Sprungmarken:* IDs im HTML nach derselben Regel wie die Blöcke, gleicher Dialekt | +| ☐ | *Sprungmarken:* HTML nach jeder Änderung neu erzeugt; Konsistenz einmal geprüft | + +--- + +## 9. Rezepte **Notizen nach zwei Begriffen im selben Absatz durchsuchen** @@ -665,7 +768,7 @@ HTML-Seiten (`dialect="python-markdown"`). --- -## 9. Fehlermeldungen +## 10. Fehlermeldungen Ungültige Anfragen liefern einen festen Code (für eigene Übersetzungen), eine Meldung und die Position (0-basiert; das Widget zeigt sie 1-basiert an). @@ -695,7 +798,7 @@ mdgrep: Missing closing parenthesis --- -## 10. Fehlersuche +## 11. Fehlersuche | Problem | Ursache / Lösung | |---|---| @@ -713,7 +816,7 @@ mdgrep: Missing closing parenthesis --- -## 11. Entwicklung & neue Versionen +## 12. Entwicklung & neue Versionen ```bash git clone https://kitux.de/forgejo/dschlueter/mdgrep.git && cd mdgrep From aec824d8bebb0538067e6fd9c3c590594bf8229c Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 12 Oct 2026 21:34:51 +0200 Subject: [PATCH 3/6] docs: add CHANGELOG.md and documentation overview - CHANGELOG.md (Keep a Changelog, German) for 0.1.0, 0.1.1 and unreleased documentation work, incl. "Beim Update zu tun" - README: documentation table (manual, how it works, changelog, license) - manual: release steps now include updating CHANGELOG/FUNKTIONSWEISE Co-Authored-By: Claude Opus 5.5 --- BEDIENUNGSANLEITUNG.md | 8 ++++--- CHANGELOG.md | 51 ++++++++++++++++++++++++++++++++++++++++++ README.md | 9 +++++++- 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 720931c..ad74c54 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -840,9 +840,11 @@ src/mdgrep/ Neue Version veröffentlichen: 1. Version in `pyproject.toml` **und** `src/mdgrep/__init__.py` erhöhen (bei Widget-Änderungen auch im Kopf von `mdgrep-widget.js`) -2. Tests laufen lassen: `.venv/bin/pytest` -3. Commit, Tag und Push: `git tag -a v0.1.2 -m "mdgrep 0.1.2" && git push origin main --tags` -4. In den nutzenden Projekten den Tag in `requirements.txt` anpassen und `pip install -r requirements.txt` +2. `CHANGELOG.md`: Abschnitt `[Unreleased]` in die neue Version umbenennen (mit Datum), „Beim Update zu tun" + ergänzen; bei Änderungen am inneren Aufbau auch `FUNKTIONSWEISE.md` aktualisieren +3. Tests laufen lassen: `.venv/bin/pytest` +4. Commit, Tag und Push: `git tag -a v0.1.2 -m "mdgrep 0.1.2" && git push origin main --tags` +5. In den nutzenden Projekten den Tag in `requirements.txt` anpassen und `pip install -r requirements.txt` ausführen; laufende Dienste neu starten. Versionsregel: Änderungen an öffentlichen Funktionen, am JSON-Format oder an Widget-Optionen, die diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a22163c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +# Änderungsprotokoll + +Alle nennenswerten Änderungen an mdgrep. Format nach [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), +Versionen nach [Semantic Versioning](https://semver.org/lang/de/) (vor 1.0: Nebenversion = möglicher Bruch). + +Jede Version ist als Git-Tag verfügbar: +`pip install "mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@vX.Y.Z"` + +## [Unreleased] + +### Dokumentation +- `BEDIENUNGSANLEITUNG.md` (deutsch): Installation, Suchsyntax, Kommandozeile, Python-API, Web-API, Widget, + Serverbetrieb, Voraussetzungen an die Dokumente, Rezepte, Fehlercodes, Fehlersuche, Release-Ablauf. +- `FUNKTIONSWEISE.md`: interner Aufbau, Datenfluss und Designentscheidungen. +- `CHANGELOG.md` (diese Datei). + +## [0.1.1] – 2026-10-12 + +### Neu +- `split_markdown()` / `load_markdown()`: Parameter `dialect="commonmark" | "python-markdown"`, damit Listen-Blöcke + zur HTML-Struktur des verwendeten Renderers passen (Python-Markdown: Liste braucht Leerzeile davor, + Verschachtelung erst ab 4 Leerzeichen). + +### Geändert +- Deutlich schneller: der durchsuchte Text (Titel + Text) wird beim Laden einmal vorberechnet; das + Regex-Zeitlimit gilt nur noch für `/regex/`-Terme (Wörter und Phrasen laufen linear). Typische Suchen über + 26 000 Blöcke: ca. 30–140 ms statt 250–700 ms. + +### Beim Update zu tun +- Nichts; voll kompatibel zu 0.1.0. Widget-Dateien unverändert (Kopfzeile meldet weiterhin v0.1.0). + +## [0.1.0] – 2026-10-12 + +Erstveröffentlichung. + +### Neu +- Suchsprache: Wortbestandteile, `"Phrasen"`, `/reguläre Ausdrücke/`, `AND`/`OR`/`NOT` (auch `UND`/`ODER`/ + `NICHT`, `&`, `|`, `-`, `!`) mit Klammern; Groß-/Kleinschreibung egal; Fehler mit festem Code und Position; + Schutz vor katastrophalen regulären Ausdrücken durch Zeitlimits. +- `SearchEngine` über Blöcke im Speicher: Filter, eigene Prädikate, Gruppierung, Sortierung, Seiten, + Zeitbudget, automatisches Neuladen, Ausschnitte mit Hervorhebung. +- Markdown-Zerleger mit den Modi `file`, `heading`, `list`, `paragraph`; Markdown → Text. +- Kommandozeile `mdgrep` (grep-artig, JSON-Ausgabe, grep-Rückgabewerte). +- Flask-Blueprint/-App mit JSON-API und Auslieferung der Widget-Dateien; Entwicklungsserver + `python -m mdgrep.web`. +- JavaScript-Such-Widget ohne Framework: Live-Suche, URL-Zustand, Filter, „Mehr laden", Syntaxhilfe, + deutsch/englisch, Gestaltung über CSS-Variablen; `export_static()` für statische Seiten. + +[Unreleased]: https://kitux.de/forgejo/dschlueter/mdgrep/compare/v0.1.1...main +[0.1.1]: https://kitux.de/forgejo/dschlueter/mdgrep/compare/v0.1.0...v0.1.1 +[0.1.0]: https://kitux.de/forgejo/dschlueter/mdgrep/src/tag/v0.1.0 diff --git a/README.md b/README.md index 377767f..1c92b7a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,14 @@ Boolean full-text search over Markdown (and any other text) – as a **Python li - protection against catastrophic regular expressions (time limit per search) - no search index: blocks live in memory, a few MB of text are searched in milliseconds -Ausführliche deutsche Anleitung: [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md) +## Documentation + +| Document | Content | +|---|---| +| [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md) | User manual (German): installation, syntax, CLI, API, widget, operation, troubleshooting | +| [FUNKTIONSWEISE.md](FUNKTIONSWEISE.md) | How it works (German): architecture, data flow, design decisions, limits | +| [CHANGELOG.md](CHANGELOG.md) | Changes per version and what to do when updating | +| [LICENSE](LICENSE) | MIT | ## Installation From 8bc2646dd5b1fbdb2678f3732611dc3ff6e046fd Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 12 Oct 2026 21:34:51 +0200 Subject: [PATCH 4/6] docs: add FUNKTIONSWEISE.md (how mdgrep works) Architecture and data flow (Mermaid), modules, query language internals (tokenizer rules, grammar, term compilation), evaluation and timeouts, highlighting and snippets, Markdown splitting and dialects, search engine pipeline, Web API and widget, design decisions with rejected alternatives, limits and extension ideas. Co-Authored-By: Claude Opus 5.5 --- FUNKTIONSWEISE.md | 330 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 FUNKTIONSWEISE.md diff --git a/FUNKTIONSWEISE.md b/FUNKTIONSWEISE.md new file mode 100644 index 0000000..123e319 --- /dev/null +++ b/FUNKTIONSWEISE.md @@ -0,0 +1,330 @@ +# mdgrep – Funktionsweise + +Dieses Dokument erklärt, **wie mdgrep innen arbeitet und warum es so gebaut ist**. Für die Benutzung siehe +[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md), für Änderungen je Version [CHANGELOG.md](CHANGELOG.md). + +Stand: Version 0.1.1 + +--- + +## Inhaltsverzeichnis + +1. [Überblick](#1-überblick) +2. [Module](#2-module) +3. [Suchsprache: vom Text zum Syntaxbaum](#3-suchsprache-vom-text-zum-syntaxbaum) +4. [Auswertung und Zeitlimit](#4-auswertung-und-zeitlimit) +5. [Hervorhebung und Ausschnitte](#5-hervorhebung-und-ausschnitte) +6. [Markdown-Zerlegung](#6-markdown-zerlegung) +7. [Suchmaschine](#7-suchmaschine) +8. [Web-API und Widget](#8-web-api-und-widget) +9. [Designentscheidungen](#9-designentscheidungen) +10. [Grenzen und Erweiterungsideen](#10-grenzen-und-erweiterungsideen) + +--- + +## 1. Überblick + +mdgrep hat **keinen Suchindex**. Alle Texte liegen als *Blöcke* im Speicher; jede Suche prüft jeden Block +(nach Filtern) gegen die übersetzte Suchanfrage. Das ist einfach, unterstützt beliebige reguläre Ausdrücke +und ist bei einigen Megabyte Text schnell genug. + +```mermaid +flowchart LR + subgraph Anfrage + Q["Suchtext
(rust OR go) -crypto"] --> T[Tokenizer] --> P[Parser] --> AST["Syntaxbaum
And/Or/Not/Term"] + end + subgraph Daten + D["Markdown-Dateien
oder eigene Daten"] --> L["load_markdown()
split_markdown()"] --> B["Blöcke
(title, text, meta)"] + end + AST --> E["SearchEngine.search()"] + B --> E + E --> F[Filter] --> M["Treffer prüfen
(mit Zeitbudget)"] --> S["sortieren, gruppieren,
Seite ausschneiden"] + S --> H["Fundstellen (Spans)
→ Ausschnitte (Segmente)"] --> R["SearchResult
.to_dict()"] + R --> CLI["mdgrep (Terminal)"] + R --> API["Flask: GET /search (JSON)"] --> W["Widget (Browser)"] +``` + +Drei Begriffe tragen das ganze Design: + +| Begriff | Bedeutung | +|---|---| +| **Block** | kleinste durchsuchbare Einheit – ein Absatz, Listenpunkt, Abschnitt, eine Datei oder ein eigener Datensatz. Die logische Verknüpfung gilt *pro Block*. | +| **Span** | Fundstelle als `(start, ende)` im Text eines Blocks | +| **Segment** | Anzeigestück `{"t": Text, "hit": bool}`; eine Liste von Segmenten ergibt den hervorgehobenen Ausschnitt | + +--- + +## 2. Module + +| Modul | Aufgabe | Wichtigste Namen | +|---|---|---| +| `query.py` | Suchsprache: Tokenizer, Parser, Auswertung, Fundstellen | `compile_query`, `Query`, `QuerySyntaxError`, `QueryTimeout` | +| `engine.py` | Blöcke halten, filtern, prüfen, sortieren, gruppieren, Seiten | `Block`, `SearchEngine`, `SearchResult`, `Group` | +| `snippets.py` | Fundstellen → Anzeige-Segmente, Kürzen um Treffer | `segments` | +| `markdown.py` | Markdown → Blöcke; Markdown → Klartext | `load_markdown`, `split_markdown`, `markdown_to_text`, `slugify` | +| `cli.py` | Kommandozeile | `main` (Einstieg `mdgrep`) | +| `web.py` | Flask-Blueprint/-App, Entwicklungsserver | `create_blueprint`, `create_app` | +| `static/` | Browser-Widget | `mdgrep-widget.js` (`MdGrep.mount`), `mdgrep-widget.css` | +| `__init__.py` | öffentliche API, `export_static()` | | + +Abhängigkeiten zeigen nur in eine Richtung: `engine` nutzt `query` und `snippets`, `markdown` nutzt +`engine` (für `Block`), `cli` und `web` nutzen alles. `query` und `snippets` hängen von nichts im Paket ab. +Nur `web.py` braucht Flask; der Rest kommt mit dem Paket `regex` aus. + +--- + +## 3. Suchsprache: vom Text zum Syntaxbaum + +### 3.1 Tokenizer (`_tokenize`) + +Der Suchtext wird Zeichen für Zeichen in Tokens zerlegt: + +| Zeichen am Token-Anfang | Token | +|---|---| +| Leerraum | wird übersprungen | +| `(` `)` | Klammer | +| `&` / `&&`, `\|` / `\|\|` | `AND` / `OR` | +| `!` | `NOT` | +| `-` **gefolgt von Nicht-Leerraum** | `NOT` (ein einzelnes `-` mit Leerzeichen danach ist ein normales Suchwort) | +| `"` … `"` | Phrase; `\"` steht für ein Anführungszeichen | +| `/` … `/` | regulärer Ausdruck; `\/` wird zu `/`, andere `\x` bleiben erhalten | +| sonst | Wort bis zum nächsten Leerraum, `(`, `)` oder `"` | + +Ein Wort, das genau `AND`, `OR`, `NOT`, `UND`, `ODER` oder `NICHT` lautet, wird zum Operator – **nur in +Großbuchstaben**. Weil `-`, `&`, `|`, `!` und `/` nur *am Token-Anfang* Sonderbedeutung haben, bleiben Wörter +wie `e-mail`, `AT&T` oder `TCP/IP` normale Suchwörter. + +### 3.2 Parser (rekursiver Abstieg) + +```text +query := or_expr EOF +or_expr := and_expr ( OR and_expr )* +and_expr := not_expr ( [AND] not_expr )* ← AND darf fehlen (Leerzeichen = UND) +not_expr := NOT not_expr | primary +primary := TERM | "(" or_expr ")" +``` + +Daraus ergibt sich der Vorrang **NOT > AND > OR**. Ein implizites UND wird erkannt, wenn nach einem Ausdruck +direkt ein Term, eine öffnende Klammer oder ein NOT folgt. + +Fehler werden als `QuerySyntaxError(message, position, code)` gemeldet; `position` zeigt auf das Token (bzw. die +öffnende Klammer oder das Textende), `code` ist stabil und übersetzbar (Liste in der Bedienungsanleitung). + +Nebenbei merkt sich der Parser für jeden Term, ob er unter einer **ungeraden Anzahl von NOT** steht +(`negated`). Das braucht später die Hervorhebung: `rust -crypto` markiert nur „rust". + +### 3.3 Terme werden zu regulären Ausdrücken + +| Term | Muster | Beispiel | +|---|---|---| +| Wort | `regex.escape(wort)` | `c++` → `c\+\+` | +| Phrase | Wörter maskiert, verbunden mit `\s+` | `"open source"` → `open\s+source` (trifft auch über Zeilenumbrüche) | +| Regex | unverändert | `/llms?\b/` | + +Alle Muster werden mit `IGNORECASE | UNICODE` des PyPI-Moduls **`regex`** kompiliert (Umlaute inklusive). +Ungültige reguläre Ausdrücke fallen schon hier als `invalid_regex` auf – nicht erst bei der Suche. + +--- + +## 4. Auswertung und Zeitlimit + +Der Syntaxbaum besteht aus `Term`, `Not`, `And`, `Or`; jeder Knoten hat `evaluate(text, deadline)`. +`And`/`Or` nutzen `all()`/`any()` und brechen daher früh ab. + +**Zeitlimit:** Bestimmte reguläre Ausdrücke brauchen exponentiell lange („katastrophales Backtracking", +z. B. `/(a|aa)+$/`). Das Modul `regex` kann jede Suche mit `timeout=` abbrechen. Der Parameter ist aber teuer +(etwa vierfache Laufzeit pro Aufruf), daher: + +- **Wörter und Phrasen** laufen **ohne** Zeitlimit – maskierte Literale und `\s+` sind linear, können also nicht + entarten. +- **Reguläre Ausdrücke** bekommen als Zeitlimit die Restzeit bis zur Frist (`deadline`). +- `Query.matches()` prüft die Frist **einmal pro Aufruf**; ist sie abgelaufen, folgt `QueryTimeout`. + +Die Suchmaschine setzt eine Frist für die ganze Suche (`time_budget`). Läuft sie ab, endet die Schleife und +das Ergebnis trägt `truncated = True` – der Server bleibt erreichbar, der Nutzer bekommt ein Teilergebnis. + +--- + +## 5. Hervorhebung und Ausschnitte + +**Fundstellen (`Query.spans`)**: Für jeden *positiven* Term (nicht unter NOT) werden alle Treffer gesucht, +Treffer der Länge 0 (z. B. von `/x*/`) verworfen, dann sortiert und überlappende oder angrenzende Bereiche +zusammengefasst. Aus `open OR source` auf „open source" werden so zwei getrennte Markierungen, aus +`abc OR bcd` auf „abcd" eine durchgehende. + +**Ausschnitte (`snippets.segments`)**: Ist der Text kürzer als `max_chars`, wird er komplett in Segmente +zerlegt. Sonst: + +1. Um jede Fundstelle wird ein Fenster von `context` Zeichen gelegt. +2. Fensterränder werden an Wortgrenzen verschoben (höchstens 20 Zeichen weit), damit keine Wörter zerschnitten + werden. +3. Überlappende Fenster verschmelzen. +4. Fenster werden aufgenommen, solange das Budget `max_chars` reicht – das **erste Fenster immer**, damit + mindestens ein Treffer sichtbar ist. +5. Lücken und abgeschnittene Enden werden durch `" … "` markiert. + +Ohne Fundstelle im Text (Treffer nur im Titel) wird der Textanfang gezeigt. + +--- + +## 6. Markdown-Zerlegung + +`split_markdown()` arbeitet **zeilenbasiert** mit einem kleinen Scanner, nicht mit einem vollständigen +Markdown-Parser. Der Scanner liefert je Zeile: Index, Inhalt, „in Code-Block?" und ggf. die erkannte +Überschrift (`#` bis `######`). Zeilen innerhalb von Code-Blöcken (` ``` ` oder `~~~`) werden nie als +Überschrift oder Listenpunkt gedeutet. + +| Modus | Regel | +|---|---| +| `file` | eine Datei = ein Block; Titel = erste Überschrift | +| `heading` | neuer Block bei jeder Überschrift; Titel = Überschrift, Anker = `slugify(Überschrift)` | +| `paragraph` | neuer Block nach jeder Leerzeile (außerhalb von Code-Blöcken); Trennlinien werden übersprungen | +| `list` | neuer Block bei jedem Listenpunkt der obersten Ebene; eingerückte Zeilen und Zeilen direkt darunter („lazy continuation") gehören dazu; Text außerhalb von Listen wird zu Absatz-Blöcken; Trennlinien und Überschriften beenden einen Block | + +**Dialekte** (nur Modus `list`): Markdown-Renderer zerlegen Listen unterschiedlich. Damit Blöcke zu den +Elementen im gerenderten HTML passen (wichtig für Sprungmarken), gibt es zwei Regelsätze: + +| | `commonmark` | `python-markdown` | +|---|---|---| +| Listenmarker | `-` `*` `+` `1.` `1)` am Zeilenanfang | dazu bis zu 3 Leerzeichen Einrückung; nummeriert nur `1.` | +| Liste direkt unter einer Absatzzeile | beginnt eine Liste | gehört zum Absatz | + +Jeder Block bekommt `meta["section"]` (letzte Überschrift darüber), `meta["kind"]` und `meta["markdown"]` +(Originaltext), dazu `source`, `line` und `id = ":"`. + +**Markdown → Klartext** (`markdown_to_text`): pro Zeile werden Listen-, Zitat- und Überschriftenmarker entfernt, +Bilder durch ihren Alternativtext, Links durch ihren Linktext ersetzt, Inline-Code, Fett, Kursiv und +Durchgestrichen ausgepackt (Unterstriche nur an Wortgrenzen, damit `snake_case` erhalten bleibt). Zeilen +bleiben erhalten, mehrere Leerzeilen werden zu einer. Durchsucht und angezeigt wird dieser Klartext. + +In den Modi `file` und `heading` wird der Titel vom Textanfang entfernt, damit er im Ergebnis nicht doppelt +erscheint. + +--- + +## 7. Suchmaschine + +### 7.1 Laden + +`SearchEngine(quelle, reload_check=…, reload_interval=…)` nimmt eine Liste von Blöcken oder eine Funktion, die +sie liefert. Beim Laden wird für jeden Block der **Suchtext** `titel + "\n" + text` einmal vorberechnet – +Titel und Text werden gemeinsam durchsucht, aber getrennt angezeigt. + +**Neuladen:** Mit Ladefunktion und `reload_check` (liefert einen beliebigen vergleichbaren Wert, etwa +Dateianzahl und neueste Änderungszeit) prüft die Suchmaschine höchstens alle `reload_interval` Sekunden, ob +sich der Wert geändert hat, und lädt dann neu. Blöcke und Suchtexte werden zusammen als ein Tupel ersetzt – +laufende Suchen sehen immer einen konsistenten Stand. + +### 7.2 Ablauf von `search()` + +```mermaid +flowchart TD + A["Anfrage kompilieren
(oder fertige Query)"] --> B["Frist = jetzt + time_budget"] + B --> C["ggf. neu laden"] + C --> D{"für jeden Block"} + D -->|"Filter / Prädikat passen nicht"| D + D -->|passt| E{"query.matches()"} + E -->|ja| F[in Trefferliste] + E -->|nein| D + E -->|QueryTimeout| G["truncated = True,
Schleife beenden"] + F --> D + D -->|fertig| H["sortieren (sort_key, reverse)"] + G --> H + H --> I["gruppieren (group_by)
Reihenfolge = erster Treffer je Gruppe"] + I --> J["Seite: Gruppen offset … offset+limit"] + J --> K["nur für die Seite:
Spans + Segmente des ersten Blocks je Gruppe"] + K --> L[SearchResult] +``` + +Wichtige Details: + +- **Filter** vergleichen `block.meta[schlüssel]` mit einem Wert oder einer Menge von Werten; leere Werte werden + ignoriert. +- **Gruppierung**: `group_by(block)` liefert einen Schlüssel oder `None` (= eigene Gruppe). Weil vorher sortiert + wird, ist der erste Block einer Gruppe der „beste" (z. B. neueste); die übrigen landen in `also`. +- **Seiten** zählen Gruppen, nicht Blöcke. `next_offset` ist `None`, wenn keine weitere Seite existiert. +- **Teure Arbeit nur für die sichtbare Seite**: Fundstellen und Ausschnitte werden nur für die bis zu + `limit` angezeigten Gruppen berechnet, nicht für alle Treffer. + +--- + +## 8. Web-API und Widget + +### 8.1 Web-API (`web.py`) + +`create_blueprint()` registriert `GET /search` und liefert die Widget-Dateien unter `/mdgrep-static/` aus. +Ablauf einer Anfrage: Parameter lesen und begrenzen (`offset ≥ 0`, `1 ≤ limit ≤ max_limit`, Länge der +Anfrage) → Filter-Parameter übernehmen (leer/`all` = keine Einschränkung, `a,b` = mehrere Werte) → +`engine.search()` → interne Metadaten (`hide_meta`, Standard `markdown`) entfernen → JSON. +`QuerySyntaxError` wird zu HTTP 400 mit `{"error": {code, message, position}}`. + +`create_app()` baut eine eigenständige Flask-App (Unicode im JSON nicht maskiert) und kann zusätzlich ein +Verzeichnis statisch ausliefern; `send_from_directory` verhindert dabei Zugriffe außerhalb dieses Verzeichnisses. + +### 8.2 Widget (`static/mdgrep-widget.js`) + +Ein einzelnes Skript ohne Abhängigkeiten, das `window.MdGrep = {mount, labels, version}` bereitstellt. + +```mermaid +sequenceDiagram + participant U as Nutzer + participant W as Widget + participant A as API + U->>W: tippt „rust -cr" + Note over W: Debounce 300 ms – nichts senden + U->>W: tippt „ypto" + W->>W: URL aktualisieren (?q=…) + W->>A: GET search?q=rust -crypto (AbortController #1) + U->>W: ändert Filter „Sprache" + W->>A: Anfrage #1 abbrechen, GET … &lang=de (#2) + A-->>W: JSON #2 + W->>W: Kästchen per createElement/textContent bauen + W-->>U: Ergebnisse, Statuszeile +``` + +- **Zustand in der URL** (`history.replaceState`): Suchbegriff und Filter stehen in `?q=…&lang=…`; beim Laden + liest das Widget sie wieder ein und sucht sofort. Dadurch sind Suchen teilbar, und die Zurück-Taste aus einem + geöffneten Treffer stellt die Suche wieder her. +- **Veraltete Antworten** werden doppelt abgefangen: `AbortController` bricht die alte Anfrage ab, und eine + Antwort wird nur angezeigt, wenn sie zur zuletzt gestarteten Anfrage gehört. +- **Sicherheit:** Alle Texte aus der Antwort werden mit `textContent` bzw. als Textknoten eingefügt, `` + entsteht per `createElement`. HTML aus Dokumenten wird so nie ausgeführt. +- **Fehler:** HTTP 400 → übersetzte Meldung zum `code` plus Position; 429 → „zu viele Anfragen"; sonst + allgemeine Meldung. +- **Gestaltung:** nur Klassen `mdgrep-*` und CSS-Variablen `--mdgrep-*` mit Standardwerten als Fallback; + `[hidden]` wird mit `!important` erzwungen, damit Seiten-CSS ausgeblendete Elemente nicht wieder einblendet. +- **Ohne Skriptcode:** Elemente mit `data-mdgrep` werden beim Laden automatisch eingerichtet. + +--- + +## 9. Designentscheidungen + +| Entscheidung | Verworfene Alternativen | Grund | +|---|---|---| +| **Kein Index – Volltext-Scan im Speicher** | Whoosh, SQLite FTS, Elasticsearch | Reguläre Ausdrücke lassen sich ohnehin nicht über einen Wortindex beantworten; Whoosh ist seit 2016 ungepflegt; bei einigen MB Text ist der Scan schnell genug (≈ 30–150 ms bei 15 MB) und es gibt nichts zu synchronisieren. | +| **PyPI-`regex` statt `re`** | Standardmodul `re` | Nur `regex` kann eine laufende Suche per `timeout=` abbrechen – Voraussetzung, um Nutzer-Regex auf einem Server zu erlauben. | +| **Zeitlimit nur für Regex-Terme** | Zeitlimit für alle Terme | Der Timeout-Parameter vervierfacht die Kosten pro Aufruf; Wörter und Phrasen können nicht entarten. Zusammen mit den vorberechneten Suchtexten gemessen 4–9× schnellere Suchen (Version 0.1.1). | +| **Logik pro Block** | pro Datei, pro Satz | Datei ist zu grob (`a AND b` trifft fast immer), Satz zu fein; der Split-Modus lässt den Nutzer die passende Einheit wählen. | +| **Titel und Text getrennt** | Titel als erste Textzeile | Sonst erscheint der Titel im Ergebnis doppelt; Programme mit eigenen Daten können einen echten Titel setzen. | +| **Zeilenbasierter Markdown-Zerleger** | vollständiger Markdown-Parser (z. B. markdown-it) | Für Blockgrenzen genügt ein kleiner Scanner, keine zusätzliche Abhängigkeit; Abweichungen einzelner Renderer deckt die `dialect`-Option ab. | +| **`dialect`-Option** | eine feste Regel | Sprungmarken funktionieren nur, wenn Blöcke exakt den HTML-Elementen des Renderers entsprechen; CommonMark und Python-Markdown unterscheiden sich bei Listen messbar (im hnews-Archiv zwei Fehlerbilder in 70 bzw. 197 Dateien). | +| **`relative_to`** | absolute Pfade | Eine Web-API soll keine Serverpfade preisgeben. | +| **Stabile Fehlercodes** | nur Fehlermeldungstext | Oberflächen können Meldungen übersetzen (Widget: de/en), ohne Texte zu parsen. | +| **Widget ohne Framework, nur `textContent`** | React/Vue, `innerHTML` | Läuft in jeder Seite, keine Build-Kette, keine XSS-Lücke durch Dokumentinhalte. | +| **Eigenes Repository, Installation per Git-Tag** | PyPI, Unterordner eines Projekts | Wiederverwendbar in beliebigen Projekten, unabhängig versioniert, ohne Veröffentlichung auf PyPI. | +| **MIT-Lizenz** | GPL | Einfache Wiederverwendung, auch in fremden Projekten. | + +--- + +## 10. Grenzen und Erweiterungsideen + +| Grenze | Auswirkung | Mögliche Erweiterung | +|---|---|---| +| Linearer Scan | Laufzeit wächst mit der Textmenge; bei Hunderten MB zu langsam | Vorfilter mit Trigramm-Index für Wörter/Phrasen | +| Jeder Server-Worker hält alle Blöcke | ca. 90 MB pro Worker bei 15 MB Markdown | gemeinsamer Speicher oder Index auf Platte | +| Keine Wortstämme | „Haus" findet nicht „Häuser" | optionale Stemming-Stufe (z. B. Snowball) | +| Keine Relevanz-Sortierung | Reihenfolge nur nach `sort_key` | Score aus Trefferzahl/Position | +| Doppelte Überschriften | `slugify` hängt kein `-1`, `-2` an (anders als GitHub) | Zähler je Datei wie GitHub | +| Nur ATX-Überschriften (`#`) | Setext-Überschriften (unterstrichen mit `===`/`---`) werden nicht erkannt | Scanner erweitern | +| Operatoren nur in Großbuchstaben | `and` ist ein Suchwort | bewusst so; ggf. Option | +| Titel und Text gemeinsam durchsucht | eine Phrase kann theoretisch über die Grenze Titel/Text hinweg treffen | Grenzzeichen statt Zeilenumbruch | From 52f7951a669a32ca56e99799b725e1269415f810 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 12 Oct 2026 21:55:33 +0200 Subject: [PATCH 5/6] docs: redraw wide diagrams so labels are readable Forgejo scales SVG diagrams down to the content width (~900 px); the left-to-right overview/deployment diagrams were 1650-2960 px wide, i.e. 5-9 px font. Redrawn top-to-bottom with multi-line labels (now 450-730 px, no scaling); replaced an overlapping self-loop label by an explicit step. Co-Authored-By: Claude Opus 5.5 --- FUNKTIONSWEISE.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/FUNKTIONSWEISE.md b/FUNKTIONSWEISE.md index 123e319..c65b0b8 100644 --- a/FUNKTIONSWEISE.md +++ b/FUNKTIONSWEISE.md @@ -29,19 +29,28 @@ mdgrep hat **keinen Suchindex**. Alle Texte liegen als *Blöcke* im Speicher; je und ist bei einigen Megabyte Text schnell genug. ```mermaid -flowchart LR +flowchart TB subgraph Anfrage - Q["Suchtext
(rust OR go) -crypto"] --> T[Tokenizer] --> P[Parser] --> AST["Syntaxbaum
And/Or/Not/Term"] + direction TB + Q["Suchtext
(rust OR go) -crypto"] --> T[Tokenizer] + T --> P[Parser] + P --> AST["Syntaxbaum
And / Or / Not / Term"] end subgraph Daten - D["Markdown-Dateien
oder eigene Daten"] --> L["load_markdown()
split_markdown()"] --> B["Blöcke
(title, text, meta)"] + direction TB + D["Markdown-Dateien
oder eigene Daten"] --> L["load_markdown()
split_markdown()"] + L --> B["Blöcke
title, text, meta"] end AST --> E["SearchEngine.search()"] B --> E - E --> F[Filter] --> M["Treffer prüfen
(mit Zeitbudget)"] --> S["sortieren, gruppieren,
Seite ausschneiden"] - S --> H["Fundstellen (Spans)
→ Ausschnitte (Segmente)"] --> R["SearchResult
.to_dict()"] - R --> CLI["mdgrep (Terminal)"] - R --> API["Flask: GET /search (JSON)"] --> W["Widget (Browser)"] + E --> F["Filter"] + F --> M["Treffer prüfen
(mit Zeitbudget)"] + M --> S["sortieren, gruppieren,
Seite ausschneiden"] + S --> H["Fundstellen (Spans)
→ Ausschnitte (Segmente)"] + H --> R["SearchResult
.to_dict()"] + R --> CLI["mdgrep
(Terminal)"] + R --> API["Flask
GET /search"] + API --> W["Widget
(Browser)"] ``` Drei Begriffe tragen das ganze Design: From 9c6b78ec366782473de0a9e761f1508b479e7fd6 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Thu, 15 Oct 2026 00:05:29 +0200 Subject: [PATCH 6/6] release: mdgrep 0.1.2 (documentation) - version 0.1.2 in pyproject.toml and __init__.py - CHANGELOG: [Unreleased] -> [0.1.2] with "Beim Update zu tun" - install examples and version notes in README, BEDIENUNGSANLEITUNG, FUNKTIONSWEISE point to v0.1.2 - no code changes; widget files unchanged (v0.1.0) Co-Authored-By: Claude Opus 5.5 --- BEDIENUNGSANLEITUNG.md | 10 +++++----- CHANGELOG.md | 9 ++++++++- FUNKTIONSWEISE.md | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/mdgrep/__init__.py | 2 +- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index ad74c54..ba01f0d 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -11,7 +11,7 @@ sich auf vier Arten nutzen: | **Web-API** (Flask) | JSON-Suchschnittstelle für Webseiten und andere Dienste | | **Such-Widget** (JavaScript) | fertige Suchmaske mit Live-Ergebnissen für jede Webseite | -Version: 0.1.1 · Lizenz: MIT · Repository: https://kitux.de/forgejo/dschlueter/mdgrep +Version: 0.1.2 · Lizenz: MIT · Repository: https://kitux.de/forgejo/dschlueter/mdgrep --- @@ -38,22 +38,22 @@ Voraussetzung: Python 3.10 oder neuer, `git` (für die Installation aus dem Repo ```bash # nur Bibliothek + Kommandozeile -pip install "mdgrep @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1" +pip install "mdgrep @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.2" # zusätzlich Web-API (installiert Flask) -pip install "mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1" +pip install "mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.2" ``` In einer `requirements.txt`: ``` -mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1 +mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.2 ``` Prüfen: ```bash -mdgrep --version # mdgrep 0.1.1 +mdgrep --version # mdgrep 0.1.2 python -c "import mdgrep; print(mdgrep.__version__)" ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index a22163c..d929804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,18 @@ Jede Version ist als Git-Tag verfügbar: ## [Unreleased] +## [0.1.2] – 2026-10-15 + ### Dokumentation - `BEDIENUNGSANLEITUNG.md` (deutsch): Installation, Suchsyntax, Kommandozeile, Python-API, Web-API, Widget, Serverbetrieb, Voraussetzungen an die Dokumente, Rezepte, Fehlercodes, Fehlersuche, Release-Ablauf. - `FUNKTIONSWEISE.md`: interner Aufbau, Datenfluss und Designentscheidungen. - `CHANGELOG.md` (diese Datei). +### Beim Update zu tun +- Nichts; nur Dokumentation, Code unverändert und voll kompatibel zu 0.1.1. Widget-Dateien unverändert + (Kopfzeile meldet weiterhin v0.1.0). + ## [0.1.1] – 2026-10-12 ### Neu @@ -46,6 +52,7 @@ Erstveröffentlichung. - JavaScript-Such-Widget ohne Framework: Live-Suche, URL-Zustand, Filter, „Mehr laden", Syntaxhilfe, deutsch/englisch, Gestaltung über CSS-Variablen; `export_static()` für statische Seiten. -[Unreleased]: https://kitux.de/forgejo/dschlueter/mdgrep/compare/v0.1.1...main +[Unreleased]: https://kitux.de/forgejo/dschlueter/mdgrep/compare/v0.1.2...main +[0.1.2]: https://kitux.de/forgejo/dschlueter/mdgrep/compare/v0.1.1...v0.1.2 [0.1.1]: https://kitux.de/forgejo/dschlueter/mdgrep/compare/v0.1.0...v0.1.1 [0.1.0]: https://kitux.de/forgejo/dschlueter/mdgrep/src/tag/v0.1.0 diff --git a/FUNKTIONSWEISE.md b/FUNKTIONSWEISE.md index c65b0b8..db3cce5 100644 --- a/FUNKTIONSWEISE.md +++ b/FUNKTIONSWEISE.md @@ -3,7 +3,7 @@ Dieses Dokument erklärt, **wie mdgrep innen arbeitet und warum es so gebaut ist**. Für die Benutzung siehe [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md), für Änderungen je Version [CHANGELOG.md](CHANGELOG.md). -Stand: Version 0.1.1 +Stand: Version 0.1.2 --- diff --git a/README.md b/README.md index 1c92b7a..75fe670 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ Boolean full-text search over Markdown (and any other text) – as a **Python li ## Installation ```bash -pip install "mdgrep @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1" # library + CLI -pip install "mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1" # + Flask API +pip install "mdgrep @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.2" # library + CLI +pip install "mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.2" # + Flask API ``` Requires Python ≥ 3.10. Dependencies: [`regex`](https://pypi.org/project/regex/) (and `flask` for `[web]`). diff --git a/pyproject.toml b/pyproject.toml index 338948f..7118644 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mdgrep" -version = "0.1.1" +version = "0.1.2" description = "Boolean full-text search (substrings, phrases, regex, AND/OR/NOT, parentheses) over Markdown and other text blocks, with CLI, Flask API and a JavaScript search widget" readme = "README.md" license = "MIT" diff --git a/src/mdgrep/__init__.py b/src/mdgrep/__init__.py index 6f54421..4e7000e 100644 --- a/src/mdgrep/__init__.py +++ b/src/mdgrep/__init__.py @@ -5,7 +5,7 @@ from .markdown import load_markdown, markdown_to_text, split_markdown from .query import Query, QuerySyntaxError, QueryTimeout, compile_query from .snippets import segments -__version__ = "0.1.1" +__version__ = "0.1.2" STATIC_FILES = ("mdgrep-widget.js", "mdgrep-widget.css")