Feat: Interaktiver XSL-Abhängigkeitsgraph mit vis.js und THIRD_PARTY_LICENSES aktualisiert

XslDependencyDialog mit zwei Tabs: Baumansicht (vorwärts/rückwärts-Abhängigkeiten)
und interaktiver Netzwerkgraph (vis.js in QWebEngineView mit Physics-Simulation,
Hover-Tooltips, Nachbar-Hervorhebung). Graceful Fallback wenn WebEngine fehlt.
THIRD_PARTY_LICENSES um psutil, PyInstaller, Pillow, vis-network ergänzt und
Versionen aktualisiert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 20:50:09 +01:00
parent 71fa48a514
commit 36911b111d
7 changed files with 965 additions and 14 deletions
+103
View File
@@ -95,6 +95,109 @@ class XslDependencyGraph:
return result
def get_reverse_dependencies(self, xsl_file: Path, xsl_root_dir: Path) -> set[Path]:
"""
Gibt alle Dateien zurück, die die gegebene XSL-Datei direkt oder transitiv importieren/inkludieren.
Args:
xsl_file: Absoluter Pfad zur XSL-Datei
xsl_root_dir: Wurzelverzeichnis der XSL-Dateien (für den Scan)
Returns:
set[Path]: Menge aller Dateien, die diese Datei importieren
"""
xsl_file = xsl_file.resolve()
result: set[Path] = set()
# Scanne alle XSL-Dateien im Verzeichnis
if not xsl_root_dir.exists():
return result
for candidate in xsl_root_dir.rglob("*.xsl"):
candidate = candidate.resolve()
if candidate == xsl_file:
continue
deps = self.get_dependencies(candidate)
if xsl_file in deps:
result.add(candidate)
return result
def build_full_graph(self, xsl_root_dir: Path) -> dict[Path, set[Path]]:
"""
Baut den vollständigen Abhängigkeitsgraph für alle XSL-Dateien in einem Verzeichnis auf.
Args:
xsl_root_dir: Wurzelverzeichnis der XSL-Dateien
Returns:
dict[Path, set[Path]]: Mapping von XSL-Datei zu ihren Abhängigkeiten
"""
graph: dict[Path, set[Path]] = {}
if not xsl_root_dir.exists():
return graph
for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")):
xsl_file = xsl_file.resolve()
deps = self.get_dependencies(xsl_file)
graph[xsl_file] = deps
logger.info(f"Vollständiger XSL-Graph aufgebaut: {len(graph)} Dateien")
return graph
def _get_direct_dependencies(self, xsl_file: Path) -> set[Path]:
"""
Gibt nur die direkten (nicht-transitiven) import/include-Abhängigkeiten zurück.
Args:
xsl_file: Absoluter Pfad zur XSL-Datei
Returns:
set[Path]: Menge der direkt importierten/inkludierten XSL-Dateien
"""
xsl_file = xsl_file.resolve()
result: set[Path] = set()
if not xsl_file.exists():
return result
try:
content = xsl_file.read_text(encoding="utf-8", errors="replace")
except Exception as e:
logger.warning(f"Konnte XSL-Datei nicht lesen: {xsl_file} ({e})")
return result
for match in _IMPORT_INCLUDE_PATTERN.finditer(content):
href = match.group(1)
referenced_path = (xsl_file.parent / href).resolve()
if referenced_path.exists():
result.add(referenced_path)
return result
def build_direct_graph(self, xsl_root_dir: Path) -> dict[Path, set[Path]]:
"""
Baut einen Graph mit nur direkten (nicht-transitiven) Abhängigkeiten auf.
Args:
xsl_root_dir: Wurzelverzeichnis der XSL-Dateien
Returns:
dict[Path, set[Path]]: Mapping von XSL-Datei zu ihren direkten Abhängigkeiten
"""
graph: dict[Path, set[Path]] = {}
if not xsl_root_dir.exists():
return graph
for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")):
xsl_file = xsl_file.resolve()
graph[xsl_file] = self._get_direct_dependencies(xsl_file)
logger.info(f"Direkter XSL-Graph aufgebaut: {len(graph)} Dateien")
return graph
def invalidate(self, xsl_file: Path | None = None):
"""
Invalidiert den Cache für eine bestimmte Datei oder den gesamten Cache.