WP20 - parser
This commit is contained in:
parent
234949800b
commit
0ac8a14ea7
|
|
@ -1,10 +1,11 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/core/parser.py
|
FILE: app/core/parser.py
|
||||||
DESCRIPTION: Liest Markdown-Dateien fehlertolerant (Encoding-Fallback). Trennt Frontmatter (YAML) vom Body.
|
DESCRIPTION: Liest Markdown-Dateien fehlertolerant (Encoding-Fallback). Trennt Frontmatter (YAML) vom Body.
|
||||||
VERSION: 1.7.1
|
WP-22 Erweiterung: Kanten-Extraktion mit Zeilennummern für die EdgeRegistry.
|
||||||
|
VERSION: 1.8.0
|
||||||
STATUS: Active
|
STATUS: Active
|
||||||
DEPENDENCIES: yaml, re, dataclasses, json, io, os
|
DEPENDENCIES: yaml, re, dataclasses, json, io, os
|
||||||
LAST_ANALYSIS: 2025-12-15
|
LAST_ANALYSIS: 2025-12-23
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -138,13 +139,7 @@ def _read_text_with_fallback(path: str) -> Tuple[str, str, bool]:
|
||||||
|
|
||||||
def read_markdown(path: str) -> Optional[ParsedNote]:
|
def read_markdown(path: str) -> Optional[ParsedNote]:
|
||||||
"""
|
"""
|
||||||
Liest eine Markdown-Datei fehlertolerant:
|
Liest eine Markdown-Datei fehlertolerant.
|
||||||
- Erlaubt verschiedene Encodings (UTF-8 bevorzugt, cp1252/latin-1 als Fallback).
|
|
||||||
- Schlägt NICHT mit UnicodeDecodeError fehl.
|
|
||||||
- Gibt ParsedNote(frontmatter, body, path) zurück oder None, falls die Datei nicht existiert.
|
|
||||||
|
|
||||||
Bei Decoding-Fallback wird ein JSON-Warnhinweis geloggt:
|
|
||||||
{"path": "...", "warn": "encoding_fallback_used", "used": "cp1252"}
|
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return None
|
return None
|
||||||
|
|
@ -161,10 +156,6 @@ def validate_required_frontmatter(fm: Dict[str, Any],
|
||||||
required: Tuple[str, ...] = ("id", "title")) -> None:
|
required: Tuple[str, ...] = ("id", "title")) -> None:
|
||||||
"""
|
"""
|
||||||
Prüft, ob alle Pflichtfelder vorhanden sind.
|
Prüft, ob alle Pflichtfelder vorhanden sind.
|
||||||
Default-kompatibel: ('id', 'title'), kann aber vom Aufrufer erweitert werden, z. B.:
|
|
||||||
validate_required_frontmatter(fm, required=("id","title","type","status","created"))
|
|
||||||
|
|
||||||
Hebt ValueError, falls Felder fehlen oder leer sind.
|
|
||||||
"""
|
"""
|
||||||
if fm is None:
|
if fm is None:
|
||||||
fm = {}
|
fm = {}
|
||||||
|
|
@ -178,17 +169,13 @@ def validate_required_frontmatter(fm: Dict[str, Any],
|
||||||
if missing:
|
if missing:
|
||||||
raise ValueError(f"Missing required frontmatter fields: {', '.join(missing)}")
|
raise ValueError(f"Missing required frontmatter fields: {', '.join(missing)}")
|
||||||
|
|
||||||
# Plausibilitäten: 'tags' sollte eine Liste sein, wenn vorhanden
|
|
||||||
if "tags" in fm and fm["tags"] not in (None, "") and not isinstance(fm["tags"], (list, tuple)):
|
if "tags" in fm and fm["tags"] not in (None, "") and not isinstance(fm["tags"], (list, tuple)):
|
||||||
raise ValueError("frontmatter 'tags' must be a list of strings")
|
raise ValueError("frontmatter 'tags' must be a list of strings")
|
||||||
|
|
||||||
|
|
||||||
def normalize_frontmatter(fm: Dict[str, Any]) -> Dict[str, Any]:
|
def normalize_frontmatter(fm: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Sanfte Normalisierung ohne Semantikänderung:
|
Normalisierung von Tags und anderen Feldern.
|
||||||
- 'tags' → Liste von Strings (Trim)
|
|
||||||
- 'embedding_exclude' → bool
|
|
||||||
- andere Felder unverändert
|
|
||||||
"""
|
"""
|
||||||
out = dict(fm or {})
|
out = dict(fm or {})
|
||||||
if "tags" in out:
|
if "tags" in out:
|
||||||
|
|
@ -205,15 +192,12 @@ def normalize_frontmatter(fm: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
|
||||||
# ------------------------------ Wikilinks ---------------------------- #
|
# ------------------------------ Wikilinks ---------------------------- #
|
||||||
|
|
||||||
# Basismuster für [[...]]; die Normalisierung (id vor '#', vor '|') macht extract_wikilinks
|
|
||||||
_WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]")
|
_WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]")
|
||||||
|
|
||||||
|
|
||||||
def extract_wikilinks(text: str) -> List[str]:
|
def extract_wikilinks(text: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Extrahiert Wikilinks wie [[id]], [[id#anchor]], [[id|label]], [[id#anchor|label]].
|
Extrahiert Wikilinks als einfache Liste von IDs.
|
||||||
Rückgabe sind NUR die Ziel-IDs (ohne Anchor/Label), führend/folgend getrimmt.
|
|
||||||
Keine aggressive Slug-Normalisierung (die kann später im Resolver erfolgen).
|
|
||||||
"""
|
"""
|
||||||
if not text:
|
if not text:
|
||||||
return []
|
return []
|
||||||
|
|
@ -222,12 +206,52 @@ def extract_wikilinks(text: str) -> List[str]:
|
||||||
raw = (m.group(1) or "").strip()
|
raw = (m.group(1) or "").strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
continue
|
continue
|
||||||
# Split an Pipe (Label) → links vor '|'
|
|
||||||
if "|" in raw:
|
if "|" in raw:
|
||||||
raw = raw.split("|", 1)[0].strip()
|
raw = raw.split("|", 1)[0].strip()
|
||||||
# Split an Anchor
|
|
||||||
if "#" in raw:
|
if "#" in raw:
|
||||||
raw = raw.split("#", 1)[0].strip()
|
raw = raw.split("#", 1)[0].strip()
|
||||||
if raw:
|
if raw:
|
||||||
out.append(raw)
|
out.append(raw)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def extract_edges_with_context(parsed: ParsedNote) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
WP-22: Extrahiert Wikilinks [[Ziel|Typ]] aus dem Body und speichert die Zeilennummer.
|
||||||
|
Gibt eine Liste von Dictionaries zurück, die direkt von der Ingestion verarbeitet werden können.
|
||||||
|
"""
|
||||||
|
edges = []
|
||||||
|
if not parsed or not parsed.body:
|
||||||
|
return edges
|
||||||
|
|
||||||
|
# Wir nutzen splitlines(True), um Zeilenumbrüche für die Positionsberechnung zu erhalten,
|
||||||
|
# oder einfaches splitlines() für die reine Zeilennummerierung.
|
||||||
|
lines = parsed.body.splitlines()
|
||||||
|
|
||||||
|
for line_num, line_content in enumerate(lines, 1):
|
||||||
|
for match in _WIKILINK_RE.finditer(line_content):
|
||||||
|
raw = (match.group(1) or "").strip()
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Syntax: [[Ziel|Typ]]
|
||||||
|
if "|" in raw:
|
||||||
|
parts = raw.split("|", 1)
|
||||||
|
target = parts[0].strip()
|
||||||
|
kind = parts[1].strip()
|
||||||
|
else:
|
||||||
|
target = raw.strip()
|
||||||
|
kind = "related_to" # Default-Typ
|
||||||
|
|
||||||
|
# Anchor (#) entfernen, da Relationen auf Notiz-Ebene (ID) basieren
|
||||||
|
if "#" in target:
|
||||||
|
target = target.split("#", 1)[0].strip()
|
||||||
|
|
||||||
|
if target:
|
||||||
|
edges.append({
|
||||||
|
"to": target,
|
||||||
|
"kind": kind,
|
||||||
|
"line": line_num,
|
||||||
|
"provenance": "explicit"
|
||||||
|
})
|
||||||
|
return edges
|
||||||
Loading…
Reference in New Issue
Block a user