silco 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. silco-0.1.0/PKG-INFO +78 -0
  2. silco-0.1.0/README.md +64 -0
  3. silco-0.1.0/pyproject.toml +39 -0
  4. silco-0.1.0/setup.cfg +4 -0
  5. silco-0.1.0/silco/__init__.py +15 -0
  6. silco-0.1.0/silco/core/__init__.py +31 -0
  7. silco-0.1.0/silco/core/kernel.py +209 -0
  8. silco-0.1.0/silco/core/models/__init__.py +12 -0
  9. silco-0.1.0/silco/core/models/edge.py +30 -0
  10. silco-0.1.0/silco/core/models/flow.py +6 -0
  11. silco-0.1.0/silco/core/models/group.py +24 -0
  12. silco-0.1.0/silco/core/models/node.py +42 -0
  13. silco-0.1.0/silco/core/renderers/__init__.py +35 -0
  14. silco-0.1.0/silco/core/renderers/base/__init__.py +0 -0
  15. silco-0.1.0/silco/core/renderers/base/config.py +20 -0
  16. silco-0.1.0/silco/core/renderers/base/diagram.py +179 -0
  17. silco-0.1.0/silco/core/renderers/base/graphics.py +88 -0
  18. silco-0.1.0/silco/core/renderers/base/layout.py +239 -0
  19. silco-0.1.0/silco/core/renderers/base/positioned_node.py +11 -0
  20. silco-0.1.0/silco/core/renderers/diagrams_backend.py +278 -0
  21. silco-0.1.0/silco/core/renderers/exporter/__init__.py +0 -0
  22. silco-0.1.0/silco/core/renderers/exporter/svg.py +254 -0
  23. silco-0.1.0/silco/core/renderers/style.py +3 -0
  24. silco-0.1.0/silco/core/renderers/svg_common.py +206 -0
  25. silco-0.1.0/silco/core/templates/shapes/actor.png +0 -0
  26. silco-0.1.0/silco/core/templates/shapes/actor.svg +9 -0
  27. silco-0.1.0/silco/core/templates/shapes/cache.svg +7 -0
  28. silco-0.1.0/silco/core/templates/shapes/component.svg +7 -0
  29. silco-0.1.0/silco/core/templates/shapes/database.svg +107 -0
  30. silco-0.1.0/silco/core/templates/shapes/external.svg +5 -0
  31. silco-0.1.0/silco/core/templates/shapes/group.svg +3 -0
  32. silco-0.1.0/silco/core/templates/shapes/queue.svg +5 -0
  33. silco-0.1.0/silco/core/templates/shapes/storage.svg +8 -0
  34. silco-0.1.0/silco/core/utils/__init__.py +0 -0
  35. silco-0.1.0/silco/plugins/__init__.py +8 -0
  36. silco-0.1.0/silco/plugins/ipython/__init__.py +60 -0
  37. silco-0.1.0/silco/plugins/pdf/__init__.py +54 -0
  38. silco-0.1.0/silco/plugins/renderers/__init__.py +10 -0
  39. silco-0.1.0/silco/plugins/renderers/mermaid.py +31 -0
  40. silco-0.1.0/silco/plugins/renderers/styles/__init__.py +5 -0
  41. silco-0.1.0/silco/plugins/renderers/styles/modern.py +59 -0
  42. silco-0.1.0/silco/plugins/renderers/styles/uml.py +59 -0
  43. silco-0.1.0/silco.egg-info/PKG-INFO +78 -0
  44. silco-0.1.0/silco.egg-info/SOURCES.txt +47 -0
  45. silco-0.1.0/silco.egg-info/dependency_links.txt +1 -0
  46. silco-0.1.0/silco.egg-info/entry_points.txt +3 -0
  47. silco-0.1.0/silco.egg-info/requires.txt +9 -0
  48. silco-0.1.0/silco.egg-info/top_level.txt +1 -0
  49. silco-0.1.0/tests/test_core.py +199 -0
silco-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: silco
3
+ Version: 0.1.0
4
+ Summary: Microkernel-based Python toolkit for system diagrams, SVG rendering, and plugin-driven exports
5
+ Requires-Python: >=3.14
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: diagrams>=0.25.1
8
+ Requires-Dist: graphviz>=0.20.3
9
+ Requires-Dist: pydantic>=2.13.3
10
+ Provides-Extra: notebook
11
+ Requires-Dist: ipython>=8; extra == "notebook"
12
+ Provides-Extra: pdf
13
+ Requires-Dist: cairosvg>=2.7; extra == "pdf"
14
+
15
+ # SILCO (**S**ystem **I**llustration & **L**ayout **Co**mposer)
16
+
17
+ Silco is a Python-first diagram toolkit built around a small microkernel. The core owns the document model, layout contracts, and plugin registry; renderers, styles, and presenters plug into that kernel. The default SVG renderer now uses the `diagrams` package with C4-flavored Graphviz output so the generated diagrams read like real system design documentation instead of custom-drawn boxes.
18
+
19
+ ```python
20
+ from silco import diagram
21
+
22
+ d = (
23
+ diagram("Checkout")
24
+ .node("user", "User", kind="actor")
25
+ .node("api", "API", kind="service", group="app")
26
+ .node("db", "Orders DB", kind="database", group="data")
27
+ .connect("user", "api", "HTTPS")
28
+ .connect("api", "db", "SQL")
29
+ )
30
+
31
+ svg = d.to_svg(style="modern")
32
+ ```
33
+
34
+ Runtime requirement: Graphviz must be installed and `dot` must be on `PATH`.
35
+
36
+ Built-ins are registered through the kernel on import. Styles, layouts, and optional renderers stay discoverable:
37
+
38
+ ```python
39
+ from silco import kernel
40
+
41
+ kernel.names("layouts") # ("dag", "grid")
42
+ kernel.names("renderers") # ("svg", "mermaid", ...)
43
+ kernel.names("styles") # ("modern", "uml")
44
+ kernel.discover() # optional plugin discovery
45
+ ```
46
+
47
+ Render the same diagram in different ways:
48
+
49
+ ```python
50
+ svg = d.to_svg(style="uml", layout="dag", title=True)
51
+ mermaid = d.to_mermaid()
52
+ ```
53
+
54
+ In IPython/Jupyter:
55
+
56
+ ```python
57
+ %load_ext silco.plugins.ipython
58
+ d
59
+ ```
60
+
61
+ Export as PDF with the optional CairoSVG-backed renderer plugin:
62
+
63
+ ```bash
64
+ pip install "silco[pdf]"
65
+ ```
66
+
67
+ ```python
68
+ d.save_pdf("checkout.pdf")
69
+ pdf_bytes = d.to_pdf()
70
+ ```
71
+
72
+ Plugin categories:
73
+
74
+ ```python
75
+ kernel.categories()
76
+ kernel.names("renderers")
77
+ kernel.names("presenters")
78
+ ```
silco-0.1.0/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # SILCO (**S**ystem **I**llustration & **L**ayout **Co**mposer)
2
+
3
+ Silco is a Python-first diagram toolkit built around a small microkernel. The core owns the document model, layout contracts, and plugin registry; renderers, styles, and presenters plug into that kernel. The default SVG renderer now uses the `diagrams` package with C4-flavored Graphviz output so the generated diagrams read like real system design documentation instead of custom-drawn boxes.
4
+
5
+ ```python
6
+ from silco import diagram
7
+
8
+ d = (
9
+ diagram("Checkout")
10
+ .node("user", "User", kind="actor")
11
+ .node("api", "API", kind="service", group="app")
12
+ .node("db", "Orders DB", kind="database", group="data")
13
+ .connect("user", "api", "HTTPS")
14
+ .connect("api", "db", "SQL")
15
+ )
16
+
17
+ svg = d.to_svg(style="modern")
18
+ ```
19
+
20
+ Runtime requirement: Graphviz must be installed and `dot` must be on `PATH`.
21
+
22
+ Built-ins are registered through the kernel on import. Styles, layouts, and optional renderers stay discoverable:
23
+
24
+ ```python
25
+ from silco import kernel
26
+
27
+ kernel.names("layouts") # ("dag", "grid")
28
+ kernel.names("renderers") # ("svg", "mermaid", ...)
29
+ kernel.names("styles") # ("modern", "uml")
30
+ kernel.discover() # optional plugin discovery
31
+ ```
32
+
33
+ Render the same diagram in different ways:
34
+
35
+ ```python
36
+ svg = d.to_svg(style="uml", layout="dag", title=True)
37
+ mermaid = d.to_mermaid()
38
+ ```
39
+
40
+ In IPython/Jupyter:
41
+
42
+ ```python
43
+ %load_ext silco.plugins.ipython
44
+ d
45
+ ```
46
+
47
+ Export as PDF with the optional CairoSVG-backed renderer plugin:
48
+
49
+ ```bash
50
+ pip install "silco[pdf]"
51
+ ```
52
+
53
+ ```python
54
+ d.save_pdf("checkout.pdf")
55
+ pdf_bytes = d.to_pdf()
56
+ ```
57
+
58
+ Plugin categories:
59
+
60
+ ```python
61
+ kernel.categories()
62
+ kernel.names("renderers")
63
+ kernel.names("presenters")
64
+ ```
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "silco"
7
+ version = "0.1.0"
8
+ description = "Microkernel-based Python toolkit for system diagrams, SVG rendering, and plugin-driven exports"
9
+ readme = "README.md"
10
+ requires-python = ">=3.14"
11
+ dependencies = [
12
+ "diagrams>=0.25.1",
13
+ "graphviz>=0.20.3",
14
+ "pydantic>=2.13.3",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ notebook = [
19
+ "ipython>=8",
20
+ ]
21
+ pdf = [
22
+ "cairosvg>=2.7",
23
+ ]
24
+
25
+ [project.entry-points."silco.plugins"]
26
+ ipython = "silco.plugins.ipython"
27
+ pdf = "silco.plugins.pdf"
28
+
29
+ [tool.setuptools]
30
+ include-package-data = true
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["silco*"]
34
+
35
+ [tool.setuptools.package-data]
36
+ silco = [
37
+ "core/templates/shapes/*.svg",
38
+ "core/templates/shapes/*.png",
39
+ ]
silco-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ from silco.core import Canvas, Diagram, Edge, Element, Flow, Group, Node, PluginInfo, RenderConfig, diagram, kernel
2
+
3
+ __all__ = [
4
+ "Canvas",
5
+ "Diagram",
6
+ "Edge",
7
+ "Element",
8
+ "Flow",
9
+ "Group",
10
+ "Node",
11
+ "PluginInfo",
12
+ "RenderConfig",
13
+ "diagram",
14
+ "kernel",
15
+ ]
@@ -0,0 +1,31 @@
1
+ from silco.core.renderers.base.config import RenderConfig
2
+ from silco.core.renderers.base.diagram import Diagram, diagram
3
+ from silco.core.renderers.base.layout import Layout
4
+ from silco.core.renderers.base.positioned_node import PositionedNode
5
+ from silco.core.renderers.base.graphics import Canvas, Element
6
+ from silco.core.kernel import PLUGIN_CATEGORIES, PluginCategory, PluginInfo, SilcoKernel, kernel
7
+ from silco.core.models import Edge, Flow, Group, Node, NodeKind
8
+
9
+ __all__ = [
10
+ "Canvas",
11
+ "Diagram",
12
+ "Edge",
13
+ "Element",
14
+ "Flow",
15
+ "Group",
16
+ "Layout",
17
+ "Node",
18
+ "NodeKind",
19
+ "PLUGIN_CATEGORIES",
20
+ "PluginCategory",
21
+ "PluginInfo",
22
+ "PositionedNode",
23
+ "RenderConfig",
24
+ "SilcoKernel",
25
+ "diagram",
26
+ "kernel",
27
+ ]
28
+
29
+ # Register built-in layouts/renderers on import.
30
+ # This keeps `Diagram.to_svg()` working out of the box.
31
+ from silco.core import renderers as _renderers # noqa: F401, E402
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import inspect
5
+ import pkgutil
6
+ from collections.abc import Callable, Iterable
7
+ from dataclasses import dataclass, field
8
+ from importlib import metadata
9
+ from types import ModuleType
10
+ from typing import Any, Literal
11
+
12
+ PluginCategory = Literal["shapes", "renderers", "layouts", "presenters", "styles"]
13
+ PluginType = PluginCategory
14
+ PLUGIN_CATEGORIES: tuple[PluginCategory, ...] = ("shapes", "renderers", "layouts", "presenters", "styles")
15
+ _CATEGORY_ALIASES = {
16
+ "shape": "shapes",
17
+ "shapes": "shapes",
18
+ "renderer": "renderers",
19
+ "renderers": "renderers",
20
+ "style": "styles",
21
+ "styles": "styles",
22
+ "layout": "layouts",
23
+ "layouts": "layouts",
24
+ "presenter": "presenters",
25
+ "presenters": "presenters",
26
+ "representation": "presenters",
27
+ "representations": "presenters",
28
+ }
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class PluginInfo:
33
+ """Metadata describing a registered plugin."""
34
+
35
+ category: PluginCategory
36
+ name: str
37
+ plugin: Any
38
+ description: str | None = None
39
+ module: str | None = None
40
+ tags: tuple[str, ...] = field(default_factory=tuple)
41
+ auto_discovered: bool = False
42
+
43
+
44
+ class SilcoKernel:
45
+ """Registry and discovery hub for core and optional plugins."""
46
+
47
+ def __init__(self) -> None:
48
+ self.plugins: dict[PluginCategory, dict[str, Any]] = {category: {} for category in PLUGIN_CATEGORIES}
49
+ self._plugin_info: dict[PluginCategory, dict[str, PluginInfo]] = {category: {} for category in PLUGIN_CATEGORIES}
50
+ self._discovered_modules: set[str] = set()
51
+ self._discovered_entry_points: set[str] = set()
52
+
53
+ def register(
54
+ self,
55
+ plugin_type: str,
56
+ name: str,
57
+ plugin: Any,
58
+ *,
59
+ description: str | None = None,
60
+ module: str | None = None,
61
+ tags: Iterable[str] = (),
62
+ auto_discovered: bool = False,
63
+ ) -> Any:
64
+ category = self.normalize_category(plugin_type)
65
+ if not name.strip():
66
+ raise ValueError("plugin name cannot be empty")
67
+ plugin_name = name.strip()
68
+ self.plugins[category][plugin_name] = plugin
69
+ self._plugin_info[category][plugin_name] = PluginInfo(
70
+ category=category,
71
+ name=plugin_name,
72
+ plugin=plugin,
73
+ description=description,
74
+ module=module or getattr(plugin, "__module__", None),
75
+ tags=tuple(tags),
76
+ auto_discovered=auto_discovered,
77
+ )
78
+ return plugin
79
+
80
+ def decorator(
81
+ self,
82
+ plugin_type: str,
83
+ name: str,
84
+ *,
85
+ description: str | None = None,
86
+ tags: Iterable[str] = (),
87
+ ) -> Callable[[Any], Any]:
88
+ def register_plugin(plugin: Any) -> Any:
89
+ return self.register(plugin_type, name, plugin, description=description, tags=tags)
90
+
91
+ return register_plugin
92
+
93
+ def get(self, plugin_type: str, name: str, default: Any = None) -> Any:
94
+ category = self.normalize_category(plugin_type)
95
+ if default is None:
96
+ return self.plugins[category][name]
97
+ return self.plugins[category].get(name, default)
98
+
99
+ def info(self, plugin_type: str, name: str) -> PluginInfo:
100
+ category = self.normalize_category(plugin_type)
101
+ return self._plugin_info[category][name]
102
+
103
+ def list(self, plugin_type: str | None = None) -> dict[PluginCategory, tuple[PluginInfo, ...]] | tuple[PluginInfo, ...]:
104
+ if plugin_type is not None:
105
+ category = self.normalize_category(plugin_type)
106
+ return tuple(self._plugin_info[category].values())
107
+ return {category: tuple(self._plugin_info[category].values()) for category in PLUGIN_CATEGORIES}
108
+
109
+ def names(self, plugin_type: str) -> tuple[str, ...]:
110
+ category = self.normalize_category(plugin_type)
111
+ return tuple(self.plugins[category])
112
+
113
+ def categories(self) -> tuple[PluginCategory, ...]:
114
+ return PLUGIN_CATEGORIES
115
+
116
+ def normalize_category(self, plugin_type: str) -> PluginCategory:
117
+ try:
118
+ return _CATEGORY_ALIASES[plugin_type]
119
+ except KeyError as exc:
120
+ valid = ", ".join(PLUGIN_CATEGORIES)
121
+ raise ValueError(f"Unknown plugin category: {plugin_type}. Expected one of: {valid}") from exc
122
+
123
+ def discover(
124
+ self,
125
+ *,
126
+ namespace: str = "silco.plugins",
127
+ entry_point_group: str = "silco.plugins",
128
+ ) -> tuple[PluginInfo, ...]:
129
+ """Import plugin modules and entry points, returning newly registered plugins."""
130
+
131
+ before = self._snapshot()
132
+ self.discover_namespace(namespace)
133
+ self.discover_entry_points(entry_point_group)
134
+ return self._new_plugins_since(before)
135
+
136
+ def discover_namespace(self, namespace: str = "silco.plugins") -> tuple[ModuleType, ...]:
137
+ package = importlib.import_module(namespace)
138
+ modules: list[ModuleType] = []
139
+ package_path = getattr(package, "__path__", None)
140
+ if package_path is None:
141
+ return tuple(modules)
142
+ for module_info in pkgutil.iter_modules(package_path, f"{namespace}."):
143
+ if module_info.name in self._discovered_modules:
144
+ continue
145
+ module = importlib.import_module(module_info.name)
146
+ self._discovered_modules.add(module_info.name)
147
+ self._register_from_module(module)
148
+ modules.append(module)
149
+ return tuple(modules)
150
+
151
+ def discover_entry_points(self, group: str = "silco.plugins") -> tuple[Any, ...]:
152
+ loaded: list[Any] = []
153
+ entry_points = metadata.entry_points()
154
+ if hasattr(entry_points, "select"):
155
+ selected = entry_points.select(group=group)
156
+ else: # pragma: no cover - compatibility with old importlib.metadata
157
+ selected = entry_points.get(group, ())
158
+ for entry_point in selected:
159
+ key = f"{entry_point.group}:{entry_point.name}"
160
+ if key in self._discovered_entry_points:
161
+ continue
162
+ plugin = entry_point.load()
163
+ self._discovered_entry_points.add(key)
164
+ self._register_loaded_plugin(entry_point.name, plugin)
165
+ loaded.append(plugin)
166
+ return tuple(loaded)
167
+
168
+ def _register_from_module(self, module: ModuleType) -> None:
169
+ register = getattr(module, "register_plugins", None)
170
+ if callable(register):
171
+ register(self)
172
+
173
+ def _register_loaded_plugin(self, name: str, plugin: Any) -> None:
174
+ if inspect.ismodule(plugin):
175
+ self._register_from_module(plugin)
176
+ return
177
+ register = getattr(plugin, "register_plugins", None)
178
+ if callable(register):
179
+ register(self)
180
+ return
181
+ category = getattr(plugin, "silco_plugin_category", None)
182
+ plugin_name = getattr(plugin, "silco_plugin_name", name)
183
+ if category is None and callable(plugin):
184
+ plugin(self)
185
+ return
186
+ if category is None:
187
+ raise ValueError(f"Discovered plugin {name!r} does not declare a Silco category")
188
+ self.register(
189
+ category,
190
+ plugin_name,
191
+ plugin,
192
+ description=getattr(plugin, "silco_plugin_description", None),
193
+ tags=getattr(plugin, "silco_plugin_tags", ()),
194
+ auto_discovered=True,
195
+ )
196
+
197
+ def _snapshot(self) -> set[tuple[PluginCategory, str]]:
198
+ return {(category, name) for category, plugins in self.plugins.items() for name in plugins}
199
+
200
+ def _new_plugins_since(self, snapshot: set[tuple[PluginCategory, str]]) -> tuple[PluginInfo, ...]:
201
+ new_plugins: list[PluginInfo] = []
202
+ for category, infos in self._plugin_info.items():
203
+ for name, info in infos.items():
204
+ if (category, name) not in snapshot:
205
+ new_plugins.append(info)
206
+ return tuple(new_plugins)
207
+
208
+
209
+ kernel = SilcoKernel()
@@ -0,0 +1,12 @@
1
+ from silco.core.models.edge import Edge
2
+ from silco.core.models.flow import Flow
3
+ from silco.core.models.group import Group
4
+ from silco.core.models.node import Node, NodeKind
5
+
6
+ __all__ = [
7
+ "Edge",
8
+ "Flow",
9
+ "Group",
10
+ "Node",
11
+ "NodeKind",
12
+ ]
@@ -0,0 +1,30 @@
1
+ from typing import Any
2
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
3
+
4
+
5
+ class Edge(BaseModel):
6
+ """A directed relationship between two nodes."""
7
+
8
+ model_config = ConfigDict(extra="forbid")
9
+
10
+ source: str = Field(min_length=1)
11
+ target: str = Field(min_length=1)
12
+ label: str | None = None
13
+ protocol: str | None = None
14
+ bidirectional: bool = False
15
+ metadata: dict[str, Any] = Field(default_factory=dict)
16
+
17
+ @field_validator("source", "target")
18
+ @classmethod
19
+ def normalize_endpoint(cls, value: str) -> str:
20
+ value = value.strip()
21
+ if not value:
22
+ raise ValueError("edge endpoint cannot be empty")
23
+ return value
24
+
25
+ @property
26
+ def display_label(self) -> str | None:
27
+ if self.label and self.protocol:
28
+ return f"{self.label} ({self.protocol})"
29
+ return self.label or self.protocol
30
+
@@ -0,0 +1,6 @@
1
+ from silco.core.models.edge import Edge
2
+
3
+
4
+ class Flow(Edge):
5
+ """A directed flow between two nodes."""
6
+
@@ -0,0 +1,24 @@
1
+ from typing import Any
2
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
3
+
4
+
5
+ class Group(BaseModel):
6
+ """A visual boundary for a set of related nodes."""
7
+
8
+ model_config = ConfigDict(extra="forbid")
9
+
10
+ id: str = Field(min_length=1)
11
+ label: str | None = None
12
+ metadata: dict[str, Any] = Field(default_factory=dict)
13
+
14
+ @field_validator("id")
15
+ @classmethod
16
+ def normalize_identifier(cls, value: str) -> str:
17
+ value = value.strip()
18
+ if not value:
19
+ raise ValueError("group id cannot be empty")
20
+ return value
21
+
22
+ @property
23
+ def display_label(self) -> str:
24
+ return self.label or self.id
@@ -0,0 +1,42 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
4
+
5
+ NodeKind = Literal[
6
+ "actor",
7
+ "service",
8
+ "database",
9
+ "queue",
10
+ "cache",
11
+ "storage",
12
+ "external",
13
+ "component",
14
+ ]
15
+
16
+
17
+ class Node(BaseModel):
18
+ """A system component that can be rendered in a diagram."""
19
+
20
+ model_config = ConfigDict(extra="forbid")
21
+
22
+ id: str = Field(min_length=1)
23
+ label: str | None = None
24
+ kind: NodeKind = "component"
25
+ description: str | None = None
26
+ group: str | None = None
27
+ metadata: dict[str, Any] = Field(default_factory=dict)
28
+
29
+ @field_validator("id", "group")
30
+ @classmethod
31
+ def normalize_identifier(cls, value: str | None) -> str | None:
32
+ if value is None:
33
+ return None
34
+ value = value.strip()
35
+ if not value:
36
+ raise ValueError("identifier cannot be empty")
37
+ return value
38
+
39
+ @property
40
+ def display_label(self) -> str:
41
+ return self.label or self.id
42
+
@@ -0,0 +1,35 @@
1
+ from silco.core.renderers.exporter.svg import svg_renderer
2
+ from silco.core.kernel import kernel
3
+ from silco.core.renderers.base.layout import dag_layout, grid_layout
4
+ from silco.core.renderers.svg_common import render_mermaid
5
+
6
+ kernel.register(
7
+ "layouts",
8
+ "dag",
9
+ dag_layout,
10
+ description="Layered directed-acyclic-graph layout for left-to-right system flows.",
11
+ tags=("builtin", "layout"),
12
+ )
13
+ kernel.register(
14
+ "layouts",
15
+ "grid",
16
+ grid_layout,
17
+ description="Simple grid layout for unordered component maps.",
18
+ tags=("builtin", "layout"),
19
+ )
20
+ kernel.register(
21
+ "renderers",
22
+ "svg",
23
+ svg_renderer,
24
+ description="Standalone SVG renderer for notebooks, docs, and exports.",
25
+ tags=("builtin", "renderer", "vector"),
26
+ )
27
+ kernel.register(
28
+ "renderers",
29
+ "mermaid",
30
+ render_mermaid,
31
+ description="Mermaid flowchart text renderer.",
32
+ tags=("builtin", "renderer", "text"),
33
+ )
34
+
35
+ __all__ = ["render_mermaid", "svg_renderer"]
File without changes
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class RenderConfig(BaseModel):
7
+ """Rendering knobs shared by built-in renderers."""
8
+
9
+ model_config = ConfigDict(extra="forbid")
10
+
11
+ direction: str = "LR"
12
+ width: int = Field(default=960, ge=320)
13
+ node_width: int = Field(default=168, ge=80)
14
+ node_height: int = Field(default=84, ge=48)
15
+ rank_gap: int = Field(default=72, ge=40)
16
+ node_gap: int = Field(default=48, ge=20)
17
+ margin: int = Field(default=32, ge=0)
18
+ style: str = Field(default="modern", min_length=1)
19
+ font_family: str = "ui-sans-serif, Segoe UI, sans-serif"
20
+ title: bool = True