Coverage for src / graphable / views / utils.py: 83%
48 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-16 21:32 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-16 21:32 +0000
1from shutil import which
2from typing import Callable
5def wrap_with_checksum(content: str, checksum: str, extension: str) -> str:
6 """
7 Prepend a checksum as a comment to the content based on file extension.
9 Args:
10 content: The original content string.
11 checksum: The blake2b hash to embed.
12 extension: The file extension (e.g., '.json', '.yaml').
14 Returns:
15 str: The content with the checksum comment prepended.
16 """
17 prefix = f"blake2b: {checksum}"
19 ext = extension.lower()
21 if ext == ".graphml" or ext == ".html" or ext == ".xml":
22 comment = f"<!-- {prefix} -->"
23 elif ext in (".yaml", ".yml", ".toml", ".txt", ".ascii", ".csv"):
24 comment = f"# {prefix}"
25 elif ext in (".dot", ".gv", ".d2"):
26 comment = f"// {prefix}"
27 elif ext == ".mmd" or ext == ".mermaid":
28 comment = f"%% {prefix}"
29 elif ext == ".puml":
30 comment = f"' {prefix}"
31 elif ext == ".tex":
32 comment = f"% {prefix}"
33 elif ext == ".json":
34 import json
36 try:
37 # Parse the rendered JSON string back to a dict
38 data = json.loads(content)
39 # Wrap it in a higher-order dict
40 wrapped_data = {"checksum": prefix, "graph": data}
41 # Return as a formatted JSON string
42 return json.dumps(wrapped_data, indent=2)
43 except Exception:
44 # Fallback if content isn't valid JSON for some reason
45 return f"# {prefix}\n{content}"
46 else:
47 comment = f"# {prefix}"
49 return f"{comment}\n{content}"
52def detect_engine() -> str:
53 """
54 Detect the available visualization engine based on system PATH.
55 Priority: Mermaid -> Graphviz -> D2 -> PlantUML.
57 Returns:
58 str: The name of the detected engine.
60 Raises:
61 RuntimeError: If no engine is found.
62 """
63 engines = {
64 "mermaid": "mmdc",
65 "graphviz": "dot",
66 "d2": "d2",
67 "plantuml": "plantuml",
68 }
70 for engine, executable in engines.items():
71 if which(executable):
72 return engine
74 raise RuntimeError(
75 "No rendering engine found on PATH. "
76 "Please install one of: mermaid-cli, graphviz, d2, or plantuml."
77 )
80def get_image_exporter(engine: str | None = None) -> Callable[..., None]:
81 """
82 Get the appropriate image exporter based on the engine name or auto-detection.
84 Args:
85 engine: Optional engine name (e.g., 'mermaid', 'graphviz').
87 Returns:
88 Callable: An export function that takes (graph, output_path, **kwargs).
89 """
90 from ..enums import Engine
92 if engine is None:
93 engine_val = detect_engine()
94 else:
95 # Normalize engine name
96 engine_val = engine.value if isinstance(engine, Engine) else engine.lower()
98 if engine_val == Engine.MERMAID:
99 from .mermaid import export_topology_mermaid_image as exporter
100 elif engine_val == Engine.GRAPHVIZ:
101 from .graphviz import export_topology_graphviz_image as exporter
102 elif engine_val == Engine.D2:
103 from .d2 import export_topology_d2_image as exporter
104 elif engine_val == Engine.PLANTUML:
105 from .plantuml import export_topology_plantuml_image as exporter
106 else:
107 raise ValueError(f"Unknown rendering engine: {engine_val}")
109 return exporter