Skip to content

Commit 47590c5

Browse files
committed
Add diagraming tooling
1 parent b7d6de8 commit 47590c5

File tree

6 files changed

+1148
-4
lines changed

6 files changed

+1148
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ docs/_downloads
44
docs/jupyter_execute/*
55
docs/.jupyter_cache/*
66
docs/reference
7+
docs/diagrams/build
78
output
89

910
*.log

docs/diagrams/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore the built diagrams
2+
build

docs/diagrams/config.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Configuration file for diagram generation using Kroki
2+
# Each [[diagram]] entry represents a diagram to be generated
3+
4+
# Example Excalidraw diagram configuration
5+
# [[diagram]]
6+
# file_src = "src/example.excalidraw"
7+
# output_name = "example"
8+
# format = "svg"
9+
10+
# Add your diagram configurations below
11+
[[diagram]]
12+
file_src = "src/indexing-parcels.excalidraw"
13+
output_name = "indexing-parcels"
14+
format = "svg"

docs/diagrams/main.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python
2+
"""
3+
Script to generate diagrams from source files using Kroki.
4+
5+
This script reads configuration from config.toml and processes diagram files
6+
using the kroki CLI tool to generate output images.
7+
"""
8+
9+
import subprocess
10+
import sys
11+
from pathlib import Path
12+
from typing import Literal
13+
14+
from pydantic import BaseModel, Field, field_validator
15+
16+
try:
17+
import tomllib
18+
except ImportError:
19+
import tomli as tomllib
20+
21+
22+
# Supported diagram types in Kroki
23+
DiagramType = Literal[ "actdiag", "blockdiag", "bpmn", "bytefield", "c4plantuml", "d2", "diagramsnet", "ditaa", "erd", "excalidraw", "graphviz", "mermaid", "nomnoml", "nwdiag", "packetdiag", "pikchr", "plantuml", "rackdiag", "seqdiag", "structurizr", "svgbob", "umlet", "vega", "vegalite", "wavedrom"] # fmt: skip
24+
25+
# Supported output formats
26+
OutputFormat = Literal["base64", "jpeg", "pdf", "png", "svg"]
27+
28+
29+
class DiagramConfig(BaseModel):
30+
"""Configuration for a single diagram."""
31+
32+
file_src: str = Field(
33+
..., description="Path to the source diagram file, relative to the script"
34+
)
35+
output_name: str | None = Field(
36+
None, description="Output filename without extension (defaults to input stem)"
37+
)
38+
format: OutputFormat = Field("svg", description="Output format")
39+
type: DiagramType | None = Field(
40+
None, description="Diagram type (defaults to infer from file extension)"
41+
)
42+
43+
@field_validator("file_src")
44+
@classmethod
45+
def validate_file_src(cls, v: str) -> str:
46+
"""Ensure file_src is not empty."""
47+
if not v or not v.strip():
48+
raise ValueError("file_src cannot be empty")
49+
return v
50+
51+
52+
class Config(BaseModel):
53+
"""Root configuration model."""
54+
55+
diagram: list[DiagramConfig] = Field(
56+
default_factory=list, description="List of diagrams to generate"
57+
)
58+
59+
60+
def main():
61+
"""Main function to process diagrams based on config.toml."""
62+
script_dir = Path(__file__).parent
63+
config_file = script_dir / "config.toml"
64+
build_dir = script_dir / "build"
65+
66+
# Ensure build directory exists
67+
build_dir.mkdir(parents=True, exist_ok=True)
68+
69+
# Load configuration
70+
try:
71+
with open(config_file, "rb") as f:
72+
config_data = tomllib.load(f)
73+
except FileNotFoundError:
74+
print(f"Error: Configuration file not found: {config_file}", file=sys.stderr)
75+
sys.exit(1)
76+
except Exception as e:
77+
print(f"Error reading configuration file: {e}", file=sys.stderr)
78+
sys.exit(1)
79+
80+
# Validate configuration with Pydantic
81+
try:
82+
config = Config(**config_data)
83+
except Exception as e:
84+
print(f"Error: Invalid configuration: {e}", file=sys.stderr)
85+
sys.exit(1)
86+
87+
# Check if there are any diagrams configured
88+
if not config.diagram:
89+
print("No diagrams configured in config.toml")
90+
return
91+
92+
print(f"Processing {len(config.diagram)} diagram(s)...")
93+
94+
# Process each diagram
95+
for i, diagram in enumerate(config.diagram, 1):
96+
# Get input file path (relative to script directory)
97+
input_file = script_dir / diagram.file_src
98+
if not input_file.exists():
99+
print(
100+
f"Warning: Source file not found: {input_file}, skipping",
101+
file=sys.stderr,
102+
)
103+
continue
104+
105+
# Get output configuration
106+
output_name = diagram.output_name or input_file.stem
107+
output_format = diagram.format
108+
output_file = build_dir / f"{output_name}.{output_format}"
109+
110+
# Detect diagram type from file extension or use explicit type
111+
if diagram.type:
112+
diagram_type = diagram.type
113+
else:
114+
# Infer from file extension
115+
diagram_type = input_file.suffix.lstrip(".")
116+
if diagram_type == "excalidraw":
117+
diagram_type = "excalidraw"
118+
119+
# Build kroki command
120+
cmd = [
121+
"kroki",
122+
"convert",
123+
str(input_file),
124+
"--type",
125+
diagram_type,
126+
"--format",
127+
output_format,
128+
"--out-file",
129+
str(output_file),
130+
]
131+
132+
# Execute kroki command
133+
print(
134+
f" [{i}/{len(config.diagram)}] Processing {diagram.file_src} -> {output_file.name}"
135+
)
136+
try:
137+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
138+
if result.stdout:
139+
print(f" {result.stdout.strip()}")
140+
except subprocess.CalledProcessError as e:
141+
print(
142+
f" Error processing {diagram.file_src}: {e.stderr.strip()}",
143+
file=sys.stderr,
144+
)
145+
continue
146+
except FileNotFoundError:
147+
print(
148+
"Error: 'kroki' command not found. Please ensure kroki is installed.",
149+
file=sys.stderr,
150+
)
151+
sys.exit(1)
152+
153+
print("Done!")
154+
155+
156+
if __name__ == "__main__":
157+
main()

0 commit comments

Comments
 (0)