archi-cli 0.1.0__py3-none-any.whl
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.
- archi_cli-0.1.0.dist-info/METADATA +214 -0
- archi_cli-0.1.0.dist-info/RECORD +16 -0
- archi_cli-0.1.0.dist-info/WHEEL +4 -0
- archi_cli-0.1.0.dist-info/entry_points.txt +2 -0
- archi_cli-0.1.0.dist-info/licenses/LICENSE +190 -0
- archi_tool/__init__.py +1 -0
- archi_tool/cli.py +528 -0
- archi_tool/discovery.py +137 -0
- archi_tool/engine.py +266 -0
- archi_tool/model.py +280 -0
- archi_tool/normalize.py +91 -0
- archi_tool/render.py +207 -0
- archi_tool/render_html.py +645 -0
- archi_tool/render_slides.py +650 -0
- archi_tool/validate.py +150 -0
- archi_tool/views.py +215 -0
archi_tool/render.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Render views from a .archimate model to Mermaid markdown.
|
|
2
|
+
|
|
3
|
+
Mermaid does its own auto-layout, so this renders the *content* of a view
|
|
4
|
+
(elements, nesting, drawn relations), not the pixel-exact Archi layout.
|
|
5
|
+
Nested diagram objects become subgraphs; connection line styles follow the
|
|
6
|
+
relation type. Output files carry a marker comment so stale files can be
|
|
7
|
+
cleaned up safely.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .model import FOLDER_BY_ELEMENT_TYPE, xsi_type
|
|
15
|
+
|
|
16
|
+
MARKER = "<!-- Gegenereerd door `archi render` — niet handmatig bewerken -->"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def display_model_path(model) -> str:
|
|
20
|
+
"""The model path as it should appear in generated output: relative to the
|
|
21
|
+
working directory so renders stay byte-identical across machines, even
|
|
22
|
+
when discovery resolved an absolute path via archi.toml."""
|
|
23
|
+
path = Path(model.path)
|
|
24
|
+
if not path.is_absolute():
|
|
25
|
+
# already relative (the common case): keep it as written
|
|
26
|
+
return path.as_posix()
|
|
27
|
+
try:
|
|
28
|
+
return path.relative_to(Path.cwd()).as_posix()
|
|
29
|
+
except ValueError:
|
|
30
|
+
# outside the working directory: the bare name keeps it deterministic
|
|
31
|
+
return path.name
|
|
32
|
+
|
|
33
|
+
# (fill, stroke, text) per ArchiMate layer — same palette as the ADO
|
|
34
|
+
# diagram conventions (Strategy amber, Motivation purple, Business lemon)
|
|
35
|
+
LAYER_PALETTE = {
|
|
36
|
+
"strategy": ("#FAC75A", "#D4882A", "#633806"),
|
|
37
|
+
"business": ("#FFF580", "#D4B830", "#5C4A00"),
|
|
38
|
+
"application": ("#B4E2FA", "#4A9CC9", "#0D3D57"),
|
|
39
|
+
"technology": ("#C9E7B7", "#7BAF5E", "#2E4A1E"),
|
|
40
|
+
"motivation": ("#CECBF6", "#7F77DD", "#26215C"),
|
|
41
|
+
"implementation_migration": ("#FBD5B5", "#D18A47", "#5C3305"),
|
|
42
|
+
"other": ("#D3D1C7", "#888780", "#444441"),
|
|
43
|
+
}
|
|
44
|
+
LAYER_STYLES = {
|
|
45
|
+
layer: f"fill:{fill},stroke:{stroke},color:{text}"
|
|
46
|
+
for layer, (fill, stroke, text) in LAYER_PALETTE.items()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# ArchiMate-ish approximations: circle for containment (no diamond in
|
|
50
|
+
# Mermaid), dotted for the dashed ArchiMate lines (influence, realization).
|
|
51
|
+
CONTAINMENT_TYPES = {"AggregationRelationship", "CompositionRelationship"}
|
|
52
|
+
DOTTED_TYPES = {"InfluenceRelationship", "RealizationRelationship"}
|
|
53
|
+
|
|
54
|
+
EDGE_LEGEND = ("pijlstijlen: `--o` bevat (aggregatie/compositie), "
|
|
55
|
+
"`-.->` gestippeld (beïnvloedt/realiseert), `-->` overig")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def slugify(name: str) -> str:
|
|
59
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
60
|
+
return slug or "view"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_descendant(node, ancestor) -> bool:
|
|
64
|
+
"""True when ancestor contains node (via the lxml parent chain)."""
|
|
65
|
+
parent = node.getparent()
|
|
66
|
+
while parent is not None:
|
|
67
|
+
if parent is ancestor:
|
|
68
|
+
return True
|
|
69
|
+
parent = parent.getparent()
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def escape_label(name: str) -> str:
|
|
74
|
+
return (name or "(naamloos)").replace('"', "#quot;")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def edge_syntax(rel) -> str:
|
|
78
|
+
rel_type = xsi_type(rel) if rel is not None else ""
|
|
79
|
+
label = rel.get("name") if rel is not None else None
|
|
80
|
+
if rel_type in CONTAINMENT_TYPES:
|
|
81
|
+
return "--o" # containment label (bevat) would only add noise
|
|
82
|
+
if rel_type in DOTTED_TYPES:
|
|
83
|
+
return f'-.->|"{escape_label(label)}"|' if label else "-.->"
|
|
84
|
+
return f'-->|"{escape_label(label)}"|' if label else "-->"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def clean_acc_text(text: str) -> str:
|
|
88
|
+
"""Single-line text for accTitle/accDescr (no newlines or braces)."""
|
|
89
|
+
return " ".join(text.replace("{", "(").replace("}", ")").split())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def render_view(model, diagram) -> str:
|
|
93
|
+
index = model.id_index()
|
|
94
|
+
lines = ["flowchart TD"]
|
|
95
|
+
# accessible name and description, like regelrecht does for its docs
|
|
96
|
+
lines.append(f" accTitle: {clean_acc_text(diagram.get('name') or 'View')}")
|
|
97
|
+
documentation = model.documentation(diagram)
|
|
98
|
+
if documentation:
|
|
99
|
+
lines.append(f" accDescr: {clean_acc_text(documentation)}")
|
|
100
|
+
mermaid_id_by_object = {}
|
|
101
|
+
layer_members = {}
|
|
102
|
+
counter = 0
|
|
103
|
+
|
|
104
|
+
def register(obj, element):
|
|
105
|
+
nonlocal counter
|
|
106
|
+
counter += 1
|
|
107
|
+
mermaid_id = f"n{counter}"
|
|
108
|
+
mermaid_id_by_object[obj.get("id")] = mermaid_id
|
|
109
|
+
layer = FOLDER_BY_ELEMENT_TYPE.get(xsi_type(element))
|
|
110
|
+
if layer:
|
|
111
|
+
layer_members.setdefault(layer, []).append(mermaid_id)
|
|
112
|
+
return mermaid_id
|
|
113
|
+
|
|
114
|
+
def walk(objects, depth):
|
|
115
|
+
indent = " " * depth
|
|
116
|
+
for obj in objects:
|
|
117
|
+
element = index.get(obj.get("archimateElement") or "")
|
|
118
|
+
if element is None:
|
|
119
|
+
continue # notes/groups without a model element
|
|
120
|
+
mermaid_id = register(obj, element)
|
|
121
|
+
label = escape_label(element.get("name"))
|
|
122
|
+
children = obj.findall("child")
|
|
123
|
+
if children:
|
|
124
|
+
lines.append(f'{indent}subgraph {mermaid_id}["{label}"]')
|
|
125
|
+
walk(children, depth + 1)
|
|
126
|
+
lines.append(f"{indent}end")
|
|
127
|
+
else:
|
|
128
|
+
lines.append(f'{indent}{mermaid_id}["{label}"]')
|
|
129
|
+
|
|
130
|
+
walk(diagram.findall("child"), 1)
|
|
131
|
+
|
|
132
|
+
for conn in diagram.iter("sourceConnection"):
|
|
133
|
+
source_id = mermaid_id_by_object.get(conn.get("source"))
|
|
134
|
+
target_id = mermaid_id_by_object.get(conn.get("target"))
|
|
135
|
+
if not source_id or not target_id:
|
|
136
|
+
continue
|
|
137
|
+
source_obj = index.get(conn.get("source"))
|
|
138
|
+
target_obj = index.get(conn.get("target"))
|
|
139
|
+
rel = index.get(conn.get("archimateRelationship") or "")
|
|
140
|
+
# nesting already expresses containment; skip the redundant arrow
|
|
141
|
+
if (rel is not None and xsi_type(rel) in CONTAINMENT_TYPES
|
|
142
|
+
and is_descendant(target_obj, source_obj)):
|
|
143
|
+
continue
|
|
144
|
+
lines.append(f" {source_id} {edge_syntax(rel)} {target_id}")
|
|
145
|
+
|
|
146
|
+
for layer, members in sorted(layer_members.items()):
|
|
147
|
+
lines.append(f" classDef {layer} {LAYER_STYLES[layer]}")
|
|
148
|
+
lines.append(f" class {','.join(members)} {layer}")
|
|
149
|
+
|
|
150
|
+
parts = [MARKER, "", f"# {diagram.get('name') or '(naamloze view)'}", ""]
|
|
151
|
+
documentation = model.documentation(diagram)
|
|
152
|
+
if documentation:
|
|
153
|
+
parts += [documentation, ""]
|
|
154
|
+
parts += ["```mermaid", *lines, "```", "",
|
|
155
|
+
f"*Gegenereerd uit `{display_model_path(model)}` — "
|
|
156
|
+
f"{EDGE_LEGEND}.*", ""]
|
|
157
|
+
return "\n".join(parts)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def render_index(model, entries) -> str:
|
|
161
|
+
rows = [MARKER, "", "# Views", "",
|
|
162
|
+
f"Gerenderde views uit `{display_model_path(model)}`. "
|
|
163
|
+
"Deze bestanden worden gegenereerd door `archi render`; "
|
|
164
|
+
"bewerk ze niet handmatig.", ""]
|
|
165
|
+
for name, filename in entries:
|
|
166
|
+
rows.append(f"- [{name}]({filename})")
|
|
167
|
+
rows.append("")
|
|
168
|
+
return "\n".join(rows)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def write_if_changed(path: Path, content: str) -> bool:
|
|
172
|
+
if path.exists() and path.read_text(encoding="utf-8") == content:
|
|
173
|
+
return False
|
|
174
|
+
# newline="\n" keeps LF on every platform (no CRLF churn on Windows)
|
|
175
|
+
path.write_text(content, encoding="utf-8", newline="\n")
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def render_all(model, out_dir) -> tuple[list, list]:
|
|
180
|
+
"""Render every view; returns (written, removed) path lists."""
|
|
181
|
+
out = Path(out_dir)
|
|
182
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
written, produced, entries = [], set(), []
|
|
184
|
+
|
|
185
|
+
for diagram in model.diagrams():
|
|
186
|
+
name = diagram.get("name") or diagram.get("id")
|
|
187
|
+
filename = slugify(name) + ".md"
|
|
188
|
+
path = out / filename
|
|
189
|
+
if write_if_changed(path, render_view(model, diagram)):
|
|
190
|
+
written.append(path)
|
|
191
|
+
produced.add(path.name)
|
|
192
|
+
entries.append((name, filename))
|
|
193
|
+
|
|
194
|
+
index_path = out / "README.md"
|
|
195
|
+
if write_if_changed(index_path, render_index(model, entries)):
|
|
196
|
+
written.append(index_path)
|
|
197
|
+
produced.add(index_path.name)
|
|
198
|
+
|
|
199
|
+
removed = []
|
|
200
|
+
for stale in out.glob("*.md"):
|
|
201
|
+
if stale.name in produced:
|
|
202
|
+
continue
|
|
203
|
+
first_line = stale.read_text(encoding="utf-8").split("\n", 1)[0]
|
|
204
|
+
if first_line.strip() == MARKER:
|
|
205
|
+
stale.unlink()
|
|
206
|
+
removed.append(stale)
|
|
207
|
+
return written, removed
|