flowscript-vm 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.
- flowscript/__init__.py +35 -0
- flowscript/__main__.py +4 -0
- flowscript/api.py +286 -0
- flowscript/bytecode.py +372 -0
- flowscript/cli.py +126 -0
- flowscript/compiler.py +1019 -0
- flowscript/debugger/__init__.py +0 -0
- flowscript/debugger/server.py +505 -0
- flowscript/debugger/static/app.js +626 -0
- flowscript/debugger/static/chart.js +620 -0
- flowscript/debugger/static/edit.js +513 -0
- flowscript/debugger/static/index.html +115 -0
- flowscript/debugger/static/style.css +216 -0
- flowscript/intops.py +63 -0
- flowscript/lexer.py +265 -0
- flowscript/mermaid.py +109 -0
- flowscript/parser.py +578 -0
- flowscript/printer.py +225 -0
- flowscript/runner.py +164 -0
- flowscript/sizes.py +512 -0
- flowscript/svg.py +766 -0
- flowscript/values.py +724 -0
- flowscript/vm.py +577 -0
- flowscript_vm-0.1.0.dist-info/METADATA +400 -0
- flowscript_vm-0.1.0.dist-info/RECORD +29 -0
- flowscript_vm-0.1.0.dist-info/WHEEL +5 -0
- flowscript_vm-0.1.0.dist-info/entry_points.txt +2 -0
- flowscript_vm-0.1.0.dist-info/licenses/LICENSE +21 -0
- flowscript_vm-0.1.0.dist-info/top_level.txt +1 -0
flowscript/svg.py
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
"""Static SVG flowcharts of a FlowScript program, for hosts that render them in their own pages.
|
|
2
|
+
|
|
3
|
+
svg = flowscript.to_svg(source) # the flowchart (every statement a node)
|
|
4
|
+
svg = flowscript.to_svg(source, view="states") # the state diagram
|
|
5
|
+
svg = flowscript.to_svg(source, state="trading") # highlight the current state
|
|
6
|
+
|
|
7
|
+
The result is a self-contained ``<svg>`` element: inline it in HTML (e.g. a Django template with
|
|
8
|
+
``|safe``) or save it as a .svg file. It carries its own scoped styles, so it needs no stylesheet:
|
|
9
|
+
|
|
10
|
+
* colours are CSS custom properties (``--fs-bg``, ``--fs-fg``, ``--fs-accent``, ...; see ``THEMES``)
|
|
11
|
+
that the host may override, e.g. ``.fs-chart { --fs-accent: #0a84ff }``;
|
|
12
|
+
* ``theme="auto"`` (default) follows ``prefers-color-scheme`` and a ``data-theme="light|dark"``
|
|
13
|
+
attribute on an ancestor (as on ``<html>``); ``"dark"`` and ``"light"`` fix the palette;
|
|
14
|
+
* element ids are prefixed per chart, so several charts can share a page.
|
|
15
|
+
|
|
16
|
+
Every node keeps ``data-ids`` (AST node ids), ``data-line`` (source line) and states keep
|
|
17
|
+
``data-state`` (name), so a page can add its own interactivity (highlighting, links to source).
|
|
18
|
+
|
|
19
|
+
This mirrors the debugger's chart (flowscript/debugger/static/chart.js) without the editing parts.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import math
|
|
24
|
+
|
|
25
|
+
from .printer import expr_to_str
|
|
26
|
+
|
|
27
|
+
__all__ = ["to_svg", "THEMES"]
|
|
28
|
+
|
|
29
|
+
CW = 7.23 # monospace character width at 12px
|
|
30
|
+
G = 24 # vertical gap between nodes
|
|
31
|
+
HG = 34 # horizontal gap between branches
|
|
32
|
+
NH = 34 # node height
|
|
33
|
+
|
|
34
|
+
# Colour tokens. "dark" is the debugger's palette; "light" is its daylight counterpart.
|
|
35
|
+
THEMES = {
|
|
36
|
+
"dark": {
|
|
37
|
+
"bg": "#15181e", "fg": "#d7dce5", "muted": "#7d8696", "line": "#303643", "accent": "#5aa9ff",
|
|
38
|
+
"cur": "#ffcc4d", "ok": "#4fd18b", "bad": "#ff6b6b", "kw": "#c792ea", "ev": "#ffcb6b",
|
|
39
|
+
"cmt": "#697386", "edge": "#6d7789", "stroke": "#8a94a8", "gate-text": "#e8ecf3",
|
|
40
|
+
"n-proc": "#2b3a55", "n-io": "#2f4a3a", "n-sub": "#3b3357", "n-dec": "#4a3b22",
|
|
41
|
+
"n-goto": "#5a2c3a", "n-emit": "#2a4b55", "n-timer": "#3d4423", "n-ret": "#3a3a3a",
|
|
42
|
+
"n-ev": "#55401f", "n-note": "#2a2d33", "n-end": "#444a55", "n-leaf": "#222833",
|
|
43
|
+
"n-gate": "#2a303c", "decl-bg": "#1b1f27", "state": "#3d5a80", "state-title": "#9ec5ff",
|
|
44
|
+
"state-fill": "rgba(90,169,255,.04)", "any": "#7a68b8", "any-title": "#c3b5ff",
|
|
45
|
+
"any-fill": "rgba(160,130,255,.04)", "sd-fill": "#22324a", "sd-stroke": "#5a7fb0",
|
|
46
|
+
"cur-fill": "rgba(255,204,77,.08)",
|
|
47
|
+
},
|
|
48
|
+
"light": {
|
|
49
|
+
"bg": "#ffffff", "fg": "#1f2430", "muted": "#6b7383", "line": "#d5dae2", "accent": "#1f6fd1",
|
|
50
|
+
"cur": "#c98a00", "ok": "#1f9d5c", "bad": "#d14343", "kw": "#8a3fb8", "ev": "#9a6700",
|
|
51
|
+
"cmt": "#7a8293", "edge": "#7d8696", "stroke": "#7d8696", "gate-text": "#1f2430",
|
|
52
|
+
"n-proc": "#dfe8f7", "n-io": "#dcefe2", "n-sub": "#e9e3f7", "n-dec": "#f6ead2",
|
|
53
|
+
"n-goto": "#f6dde2", "n-emit": "#d9eef2", "n-timer": "#ebefd6", "n-ret": "#e6e6e6",
|
|
54
|
+
"n-ev": "#f7e4c6", "n-note": "#f5f6f8", "n-end": "#d7dbe2", "n-leaf": "#f1f3f6",
|
|
55
|
+
"n-gate": "#e4e8ee", "decl-bg": "#f7f8fa", "state": "#8fb0d8", "state-title": "#2357a0",
|
|
56
|
+
"state-fill": "rgba(31,111,209,.04)", "any": "#8f7fd0", "any-title": "#5a46b0",
|
|
57
|
+
"any-fill": "rgba(110,90,200,.04)", "sd-fill": "#e3edf9", "sd-stroke": "#6f95c8",
|
|
58
|
+
"cur-fill": "rgba(201,138,0,.10)",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------------------------- helpers
|
|
64
|
+
def _esc(s):
|
|
65
|
+
return (str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
|
66
|
+
.replace('"', """))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _tw(s):
|
|
70
|
+
return len(s) * CW
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _clip(s, n=44):
|
|
74
|
+
return s[:n - 1] + "…" if len(s) > n else s
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _f(v):
|
|
78
|
+
v = round(v * 10) / 10
|
|
79
|
+
return str(int(v)) if v == int(v) else str(v)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _typed_params(it):
|
|
83
|
+
ptypes = it.get("ptypes") or []
|
|
84
|
+
return ", ".join("%s %s" % (ptypes[i] if i < len(ptypes) else "int", p)
|
|
85
|
+
for i, p in enumerate(it.get("params") or []))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _sub_ids(e, acc=None):
|
|
89
|
+
acc = [] if acc is None else acc
|
|
90
|
+
if not isinstance(e, dict):
|
|
91
|
+
return acc
|
|
92
|
+
if "id" in e:
|
|
93
|
+
acc.append(e["id"])
|
|
94
|
+
for k in ("left", "right", "operand", "value", "ms"):
|
|
95
|
+
if isinstance(e.get(k), dict):
|
|
96
|
+
_sub_ids(e[k], acc)
|
|
97
|
+
for a in e.get("args") or []:
|
|
98
|
+
_sub_ids(a, acc)
|
|
99
|
+
return acc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_gate(e):
|
|
103
|
+
return ((e["type"] == "binary" and e["op"] in ("and", "or", "xor")) or
|
|
104
|
+
(e["type"] == "unary" and e["op"] == "not"))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _gate_inputs(e, chain=None):
|
|
108
|
+
if e["type"] == "unary":
|
|
109
|
+
return [e["operand"]]
|
|
110
|
+
out = []
|
|
111
|
+
|
|
112
|
+
def fl(x):
|
|
113
|
+
if x["type"] == "binary" and x["op"] == e["op"]:
|
|
114
|
+
if chain is not None:
|
|
115
|
+
chain.append(x["id"])
|
|
116
|
+
fl(x["left"])
|
|
117
|
+
fl(x["right"])
|
|
118
|
+
else:
|
|
119
|
+
out.append(x)
|
|
120
|
+
fl(e)
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _Layout:
|
|
125
|
+
"""A laid-out piece: extents left/right of its centre line, height, whether it ends the flow."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, l, r, h, draw, term=False):
|
|
128
|
+
self.l, self.r, self.h, self.draw, self.term = l, r, h, draw, term
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------------------------- renderer
|
|
132
|
+
class _Chart:
|
|
133
|
+
def __init__(self, ast, prefix, state=None):
|
|
134
|
+
self.ast = ast
|
|
135
|
+
self.p = prefix
|
|
136
|
+
self.cur_state = state
|
|
137
|
+
self.out = []
|
|
138
|
+
self.funcs = {i["name"] for i in ast["items"] if i["type"] == "func"}
|
|
139
|
+
|
|
140
|
+
def P(self, s):
|
|
141
|
+
self.out.append(s)
|
|
142
|
+
|
|
143
|
+
def marker(self, name):
|
|
144
|
+
return "url(#%s-%s)" % (self.p, name)
|
|
145
|
+
|
|
146
|
+
# --- gates ------------------------------------------------------------------------------
|
|
147
|
+
def gate_tree(self, e, root=None):
|
|
148
|
+
root = e["id"] if root is None else root
|
|
149
|
+
if not _is_gate(e):
|
|
150
|
+
text = _clip(expr_to_str(e), 34)
|
|
151
|
+
w, h = _tw(text) + 18, 22
|
|
152
|
+
|
|
153
|
+
def draw(xo, ym):
|
|
154
|
+
self.P('<g class="nd k-leaf" data-ids="%s" data-dec="%s" data-line="%s"><title>%s</title>'
|
|
155
|
+
'<rect class="sh" x="%s" y="%s" width="%s" height="%s" rx="4"/>'
|
|
156
|
+
'<text x="%s" y="%s">%s</text></g>'
|
|
157
|
+
% (" ".join(map(str, _sub_ids(e))), root, e.get("line", 0), _esc(expr_to_str(e)),
|
|
158
|
+
_f(xo - w), _f(ym - h / 2), _f(w), h, _f(xo - w + 9), _f(ym + 4), _esc(text)))
|
|
159
|
+
return _Layout(w, 0, h, draw)
|
|
160
|
+
chain = []
|
|
161
|
+
inputs = _gate_inputs(e, chain)
|
|
162
|
+
kids = [self.gate_tree(x, root) for x in inputs]
|
|
163
|
+
n = len(kids)
|
|
164
|
+
GW = 42
|
|
165
|
+
GH = 26 if e["type"] == "unary" else max(30, n * 14 + 6)
|
|
166
|
+
stack_h = sum(c.h for c in kids) + 8 * (n - 1)
|
|
167
|
+
cw = max(c.l for c in kids)
|
|
168
|
+
op = "not" if e["type"] == "unary" else e["op"]
|
|
169
|
+
|
|
170
|
+
def draw(xo, ym):
|
|
171
|
+
x0, x1, y0, y1 = xo - GW, xo, ym - GH / 2, ym + GH / 2
|
|
172
|
+
y = ym - stack_h / 2
|
|
173
|
+
for k, c in enumerate(kids):
|
|
174
|
+
cy, cx = y + c.h / 2, x0 - 22
|
|
175
|
+
py = ym if n == 1 else y0 + (k + 1) * GH / (n + 1)
|
|
176
|
+
self.P('<path class="wire" d="M%s %s H%s V%s H%s"/>'
|
|
177
|
+
% (_f(cx), _f(cy), _f(cx + 11), _f(py), _f(x0 + (5 if op in ("or", "xor") else 0))))
|
|
178
|
+
c.draw(cx, cy)
|
|
179
|
+
y += c.h + 8
|
|
180
|
+
if op == "and":
|
|
181
|
+
d = "M%s %s H%s A%s %s 0 0 1 %s %s H%s Z" % (
|
|
182
|
+
_f(x0), _f(y0), _f(x1 - GH / 2), _f(GH / 2), _f(GH / 2), _f(x1 - GH / 2), _f(y1), _f(x0))
|
|
183
|
+
elif op == "not":
|
|
184
|
+
d = "M%s %s L%s %s L%s %s Z" % (_f(x0), _f(y0), _f(x1 - 9), _f(ym), _f(x0), _f(y1))
|
|
185
|
+
else:
|
|
186
|
+
xs = x0 + 5 if op == "xor" else x0
|
|
187
|
+
d = "M%s %s Q%s %s %s %s Q%s %s %s %s Q%s %s %s %s Z" % (
|
|
188
|
+
_f(xs), _f(y0), _f(xs + 11), _f(ym), _f(xs), _f(y1), _f(xs + GW * 0.55), _f(y1),
|
|
189
|
+
_f(x1), _f(ym), _f(xs + GW * 0.55), _f(y0), _f(xs), _f(y0))
|
|
190
|
+
extra = ""
|
|
191
|
+
if op == "xor":
|
|
192
|
+
extra = '<path class="sh" style="fill:none" d="M%s %s Q%s %s %s %s"/>' % (
|
|
193
|
+
_f(x0), _f(y0), _f(x0 + 11), _f(ym), _f(x0), _f(y1))
|
|
194
|
+
if op == "not":
|
|
195
|
+
extra = '<circle class="sh" cx="%s" cy="%s" r="4.5"/>' % (_f(x1 - 4.5), _f(ym))
|
|
196
|
+
self.P('<g class="nd k-gate" data-ids="%s" data-dec="%s" data-line="%s"><title>%s</title>'
|
|
197
|
+
'<path class="sh" d="%s"/>%s<text class="gate-op" x="%s" y="%s">%s</text></g>'
|
|
198
|
+
% (" ".join(map(str, [e["id"]] + chain)), root, e.get("line", 0),
|
|
199
|
+
_esc(expr_to_str(e)), d, extra, _f(x0 + (4 if op == "not" else 9)), _f(ym + 3.5),
|
|
200
|
+
op.upper()))
|
|
201
|
+
return _Layout(cw + 22 + GW, 0, max(stack_h, GH), draw)
|
|
202
|
+
|
|
203
|
+
# --- shapes -----------------------------------------------------------------------------
|
|
204
|
+
@staticmethod
|
|
205
|
+
def shape(kind, x0, y0, w, h):
|
|
206
|
+
x1, y1, ym = x0 + w, y0 + h, y0 + h / 2
|
|
207
|
+
F = _f
|
|
208
|
+
if kind == "io":
|
|
209
|
+
return '<path class="sh" d="M%s %s H%s L%s %s H%s Z"/>' % (F(x0 + 9), F(y0), F(x1), F(x1 - 9), F(y1), F(x0))
|
|
210
|
+
if kind == "sub":
|
|
211
|
+
return ('<rect class="sh" x="%s" y="%s" width="%s" height="%s"/>'
|
|
212
|
+
'<path class="sh" d="M%s %s V%s M%s %s V%s" style="fill:none"/>'
|
|
213
|
+
% (F(x0), F(y0), F(w), F(h), F(x0 + 8), F(y0), F(y1), F(x1 - 8), F(y0), F(y1)))
|
|
214
|
+
if kind == "emit":
|
|
215
|
+
return '<path class="sh" d="M%s %s H%s L%s %s L%s %s H%s Z"/>' % (
|
|
216
|
+
F(x0), F(y0), F(x1 - 13), F(x1), F(ym), F(x1 - 13), F(y1), F(x0))
|
|
217
|
+
if kind in ("goto", "ret"):
|
|
218
|
+
return '<rect class="sh" x="%s" y="%s" width="%s" height="%s" rx="%s"/>' % (F(x0), F(y0), F(w), F(h), F(h / 2))
|
|
219
|
+
if kind == "ev":
|
|
220
|
+
return '<path class="sh" d="M%s %s H%s L%s %s L%s %s H%s L%s %s Z"/>' % (
|
|
221
|
+
F(x0 + 11), F(y0), F(x1 - 11), F(x1), F(ym), F(x1 - 11), F(y1), F(x0 + 11), F(x0), F(ym))
|
|
222
|
+
if kind == "note":
|
|
223
|
+
return '<path class="sh" d="M%s %s H%s L%s %s V%s H%s Z"/>' % (
|
|
224
|
+
F(x0), F(y0), F(x1 - 9), F(x1), F(y0 + 9), F(y1), F(x0))
|
|
225
|
+
if kind == "timer":
|
|
226
|
+
return '<rect class="sh" x="%s" y="%s" width="%s" height="%s" rx="9"/>' % (F(x0), F(y0), F(w), F(h))
|
|
227
|
+
return '<rect class="sh" x="%s" y="%s" width="%s" height="%s" rx="3"/>' % (F(x0), F(y0), F(w), F(h))
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def note_layout(note):
|
|
231
|
+
if not note:
|
|
232
|
+
return None
|
|
233
|
+
lines = [_clip(l, 40) for l in str(note).split("\n")]
|
|
234
|
+
return {"w": max(_tw(l) for l in lines) + 16, "h": len(lines) * 14 + 8, "lines": lines}
|
|
235
|
+
|
|
236
|
+
def note_block(self, nl, x, y, node_id=None):
|
|
237
|
+
attrs = ' data-ids="%s"' % node_id if node_id is not None else ""
|
|
238
|
+
self.P('<g class="nd k-note"%s>%s%s</g>' % (
|
|
239
|
+
attrs, self.shape("note", x, y, nl["w"], nl["h"]),
|
|
240
|
+
"".join('<text x="%s" y="%s">%s</text>' % (_f(x + 8), _f(y + 16 + i * 14), _esc(l))
|
|
241
|
+
for i, l in enumerate(nl["lines"]))))
|
|
242
|
+
|
|
243
|
+
def draw_note(self, nl, x, y, from_x, from_y):
|
|
244
|
+
self.P('<path class="note-line" d="M%s %s H%s"/>' % (_f(from_x), _f(from_y), _f(x)))
|
|
245
|
+
self.note_block(nl, x, y)
|
|
246
|
+
|
|
247
|
+
def node(self, node, kind, text, ids=None, term=False, min_w=96, head=False):
|
|
248
|
+
label = _clip(text, 46)
|
|
249
|
+
pad = 34 if kind == "sub" else 32 if kind in ("io", "emit") else 24
|
|
250
|
+
w = max(min_w, _tw(label) + pad)
|
|
251
|
+
nl = self.note_layout(node.get("note"))
|
|
252
|
+
r = w / 2 + (18 + nl["w"] if nl else 0)
|
|
253
|
+
ids = _sub_ids(node) if ids is None else ids
|
|
254
|
+
|
|
255
|
+
def draw(cx, y):
|
|
256
|
+
x0 = cx - w / 2
|
|
257
|
+
self.P('<g class="nd k-%s%s" data-ids="%s" data-line="%s"><title>%s</title>%s'
|
|
258
|
+
'<text x="%s" y="%s" text-anchor="middle">%s</text></g>'
|
|
259
|
+
% (kind, " head" if head else "", " ".join(map(str, ids)), node.get("line", 0),
|
|
260
|
+
_esc(text), self.shape(kind, x0, y, w, NH), _f(cx), _f(y + 21), _esc(label)))
|
|
261
|
+
if nl:
|
|
262
|
+
self.draw_note(nl, cx + w / 2 + 18, y, cx + w / 2, y + NH / 2)
|
|
263
|
+
return _Layout(w / 2, r, max(NH, nl["h"] if nl else 0), draw, term)
|
|
264
|
+
|
|
265
|
+
# --- flow ---------------------------------------------------------------------------------
|
|
266
|
+
def arrow(self, x, y0, y1):
|
|
267
|
+
self.P('<path class="edge" d="M%s %s V%s" marker-end="%s"/>' % (_f(x), _f(y0), _f(y1), self.marker("arr")))
|
|
268
|
+
|
|
269
|
+
def line(self, x, y0, y1):
|
|
270
|
+
self.P('<path class="edge" d="M%s %s V%s"/>' % (_f(x), _f(y0), _f(y1)))
|
|
271
|
+
|
|
272
|
+
def seq(self, stmts):
|
|
273
|
+
items = [self.stmt(s) for s in stmts or []]
|
|
274
|
+
h, l, r = G, 0, 0
|
|
275
|
+
for it in items:
|
|
276
|
+
h += it.h + G
|
|
277
|
+
l, r = max(l, it.l), max(r, it.r)
|
|
278
|
+
term = bool(items) and items[-1].term
|
|
279
|
+
|
|
280
|
+
def draw(cx, y):
|
|
281
|
+
yy = y
|
|
282
|
+
for i in range(len(items) + 1):
|
|
283
|
+
prev_term = i > 0 and items[i - 1].term
|
|
284
|
+
if not prev_term:
|
|
285
|
+
(self.arrow if i < len(items) else self.line)(cx, yy, yy + G)
|
|
286
|
+
yy += G
|
|
287
|
+
if i < len(items):
|
|
288
|
+
items[i].draw(cx, yy)
|
|
289
|
+
yy += items[i].h
|
|
290
|
+
return _Layout(l, r, h, draw, term)
|
|
291
|
+
|
|
292
|
+
def stmt(self, s):
|
|
293
|
+
t = s["type"]
|
|
294
|
+
E = expr_to_str
|
|
295
|
+
if t == "assign":
|
|
296
|
+
return self.node(s, "proc", "%s = %s" % (s["target"], E(s["value"])))
|
|
297
|
+
if t == "local":
|
|
298
|
+
return self.node(s, "proc", "%s %s%s" % (s.get("vtype") or "int", s["name"],
|
|
299
|
+
" = " + E(s["value"]) if s.get("value") else ""))
|
|
300
|
+
if t == "call":
|
|
301
|
+
return self.node(s, "sub" if s["name"] in self.funcs else "io", E(s))
|
|
302
|
+
if t == "goto":
|
|
303
|
+
return self.node(s, "goto", "⇒ " + s["state"], term=True, min_w=80)
|
|
304
|
+
if t == "return":
|
|
305
|
+
return self.node(s, "ret", "return" + (" " + E(s["value"]) if s.get("value") else ""),
|
|
306
|
+
term=True, min_w=70)
|
|
307
|
+
if t == "emit":
|
|
308
|
+
args = s.get("args") or []
|
|
309
|
+
return self.node(s, "emit", "raise_event %s%s" % (
|
|
310
|
+
s["event"], "(%s)" % ", ".join(E(a) for a in args) if args else ""))
|
|
311
|
+
if t == "start":
|
|
312
|
+
return self.node(s, "timer", "⧗ start %s%s%s" % (s["timer"], " every " if s.get("every") else " ",
|
|
313
|
+
E(s["ms"])))
|
|
314
|
+
if t == "stop":
|
|
315
|
+
return self.node(s, "timer", "⧗ stop " + s["timer"])
|
|
316
|
+
if t == "comment":
|
|
317
|
+
return self.node({"id": s["id"], "line": s.get("line", 0)}, "note",
|
|
318
|
+
s["text"].split("\n")[0], ids=[s["id"]])
|
|
319
|
+
if t == "if":
|
|
320
|
+
return self.decision(s, 0)
|
|
321
|
+
if t == "while":
|
|
322
|
+
return self.while_(s)
|
|
323
|
+
return self.node(s, "proc", "? " + t)
|
|
324
|
+
|
|
325
|
+
def diamond_info(self, cond):
|
|
326
|
+
if _is_gate(cond):
|
|
327
|
+
gt = self.gate_tree(cond)
|
|
328
|
+
dw, dh = 56, 40
|
|
329
|
+
return {"gt": gt, "dw": dw, "dh": dh, "text": "?", "top": max(0, gt.h / 2 - dh / 2 + 4),
|
|
330
|
+
"gw": gt.l + 14}
|
|
331
|
+
text = _clip(expr_to_str(cond), 30)
|
|
332
|
+
return {"gt": None, "dw": max(80, _tw(text) + 46), "dh": 46, "text": text, "top": 0, "gw": 0}
|
|
333
|
+
|
|
334
|
+
def draw_diamond(self, cond, D, cx, y_top, extra_ids):
|
|
335
|
+
y_mid, y_bot = y_top + D["dh"] / 2, y_top + D["dh"]
|
|
336
|
+
ids = ([cond["id"]] if D["gt"] else _sub_ids(cond)) + list(extra_ids)
|
|
337
|
+
hw = D["dw"] / 2
|
|
338
|
+
self.P('<g class="nd k-dec" data-ids="%s" data-line="%s"><title>%s</title>'
|
|
339
|
+
'<path class="sh" d="M%s %s L%s %s L%s %s L%s %s Z"/>'
|
|
340
|
+
'<text x="%s" y="%s" text-anchor="middle">%s</text></g>'
|
|
341
|
+
% (" ".join(map(str, ids)), cond.get("line", 0), _esc(expr_to_str(cond)),
|
|
342
|
+
_f(cx), _f(y_top), _f(cx + hw), _f(y_mid), _f(cx), _f(y_bot), _f(cx - hw), _f(y_mid),
|
|
343
|
+
_f(cx), _f(y_mid + 4), _esc(D["text"])))
|
|
344
|
+
if D["gt"]:
|
|
345
|
+
D["gt"].draw(cx - hw - 14, y_mid)
|
|
346
|
+
self.P('<path class="wire" d="M%s %s H%s" marker-end="%s"/>'
|
|
347
|
+
% (_f(cx - hw - 14), _f(y_mid), _f(cx - hw), self.marker("arr")))
|
|
348
|
+
return y_mid, y_bot
|
|
349
|
+
|
|
350
|
+
def yes_no(self, cx, hw, y_mid, y_bot):
|
|
351
|
+
self.P('<text class="lbl-yes" x="%s" y="%s">yes</text>' % (_f(cx + 5), _f(y_bot + 11)))
|
|
352
|
+
self.P('<text class="lbl-no" x="%s" y="%s">no</text>' % (_f(cx + hw + 5), _f(y_mid - 5)))
|
|
353
|
+
|
|
354
|
+
def decision(self, s, i):
|
|
355
|
+
br = s["branches"][i]
|
|
356
|
+
D = self.diamond_info(br["cond"])
|
|
357
|
+
Y = self.seq(br["body"])
|
|
358
|
+
if i + 1 < len(s["branches"]):
|
|
359
|
+
inner = self.decision(s, i + 1)
|
|
360
|
+
|
|
361
|
+
def ndraw(cx, y, inner=inner):
|
|
362
|
+
self.arrow(cx, y, y + G)
|
|
363
|
+
inner.draw(cx, y + G)
|
|
364
|
+
N = _Layout(inner.l, inner.r, G + inner.h, ndraw, inner.term)
|
|
365
|
+
else:
|
|
366
|
+
N = self.seq(s.get("else_body") or [])
|
|
367
|
+
hw = D["dw"] / 2
|
|
368
|
+
xN = max(hw, Y.r) + HG + N.l
|
|
369
|
+
l = max(Y.l, hw + D["gw"])
|
|
370
|
+
r = xN + max(N.r, 8)
|
|
371
|
+
h = D["top"] + max(D["dh"] + Y.h, D["dh"] / 2 + N.h) + 8
|
|
372
|
+
nl = self.note_layout(s.get("note")) if i == 0 else None
|
|
373
|
+
|
|
374
|
+
def draw(cx, y):
|
|
375
|
+
y_top = y + D["top"]
|
|
376
|
+
if D["top"]:
|
|
377
|
+
self.line(cx, y, y_top)
|
|
378
|
+
y_mid, y_bot = self.draw_diamond(br["cond"], D, cx, y_top, [s["id"]] if i == 0 else [])
|
|
379
|
+
merge = y + h
|
|
380
|
+
Y.draw(cx, y_bot)
|
|
381
|
+
if not Y.term:
|
|
382
|
+
self.line(cx, y_bot + Y.h, merge)
|
|
383
|
+
self.P('<path class="edge" d="M%s %s H%s"/>' % (_f(cx + hw), _f(y_mid), _f(cx + xN)))
|
|
384
|
+
N.draw(cx + xN, y_mid)
|
|
385
|
+
if not N.term:
|
|
386
|
+
self.P('<path class="edge" d="M%s %s V%s H%s" marker-end="%s"/>'
|
|
387
|
+
% (_f(cx + xN), _f(y_mid + N.h), _f(merge), _f(cx + 1), self.marker("arr")))
|
|
388
|
+
self.yes_no(cx, hw, y_mid, y_bot)
|
|
389
|
+
if nl:
|
|
390
|
+
self.draw_note(nl, cx + r + 10, y_top, cx + hw, y_top + 6)
|
|
391
|
+
return _Layout(l, r + (nl["w"] + 20 if nl else 0), h, draw, Y.term and N.term)
|
|
392
|
+
|
|
393
|
+
def while_(self, s):
|
|
394
|
+
D = self.diamond_info(s["cond"])
|
|
395
|
+
B = self.seq(s["body"])
|
|
396
|
+
entry = 14
|
|
397
|
+
hw = D["dw"] / 2
|
|
398
|
+
L = max(B.l, hw + D["gw"]) + 16
|
|
399
|
+
R = max(hw, B.r) + 22
|
|
400
|
+
h = entry + D["top"] + D["dh"] + B.h + 18
|
|
401
|
+
nl = self.note_layout(s.get("note"))
|
|
402
|
+
|
|
403
|
+
def draw(cx, y):
|
|
404
|
+
y_top = y + entry + D["top"]
|
|
405
|
+
self.arrow(cx, y, y_top)
|
|
406
|
+
y_mid, y_bot = self.draw_diamond(s["cond"], D, cx, y_top, [s["id"]])
|
|
407
|
+
B.draw(cx, y_bot)
|
|
408
|
+
yb = y_bot + B.h
|
|
409
|
+
if not B.term:
|
|
410
|
+
self.P('<path class="edge back" d="M%s %s V%s H%s V%s H%s" marker-end="%s"/>'
|
|
411
|
+
% (_f(cx), _f(yb), _f(yb + 8), _f(cx - L), _f(y + 7), _f(cx - 2), self.marker("arr")))
|
|
412
|
+
self.P('<path class="edge" d="M%s %s H%s V%s H%s"/>'
|
|
413
|
+
% (_f(cx + hw), _f(y_mid), _f(cx + R), _f(y + h), _f(cx + 1)))
|
|
414
|
+
self.yes_no(cx, hw, y_mid, y_bot)
|
|
415
|
+
if nl:
|
|
416
|
+
self.draw_note(nl, cx + R + 14, y_top, cx + R, y_top + 6)
|
|
417
|
+
return _Layout(L + 4, R + 4 + (nl["w"] + 20 if nl else 0), h, draw)
|
|
418
|
+
|
|
419
|
+
def routine(self, node):
|
|
420
|
+
is_func = node["type"] == "func"
|
|
421
|
+
if is_func:
|
|
422
|
+
head = "func %s(%s)%s" % (node["name"], _typed_params(node),
|
|
423
|
+
" -> " + node["returns"] if node.get("returns") else "")
|
|
424
|
+
else:
|
|
425
|
+
head = "on " + node["event"] + ("(%s)" % ", ".join(node["params"]) if node.get("params") else "")
|
|
426
|
+
H = self.node(node, "sub" if is_func else "ev", head, ids=[], min_w=120, head=True)
|
|
427
|
+
B = self.seq(node["body"])
|
|
428
|
+
end_h = 0 if B.term else 22
|
|
429
|
+
|
|
430
|
+
def draw(cx, y):
|
|
431
|
+
H.draw(cx, y)
|
|
432
|
+
B.draw(cx, y + H.h)
|
|
433
|
+
if not B.term:
|
|
434
|
+
ye = y + H.h + B.h + 11
|
|
435
|
+
self.P('<g class="nd k-end" data-ids="%s" data-line="%s"><title>end of %s</title>'
|
|
436
|
+
'<circle class="sh" cx="%s" cy="%s" r="10"/>'
|
|
437
|
+
'<text class="end" x="%s" y="%s" text-anchor="middle">end</text></g>'
|
|
438
|
+
% (node["id"], node.get("line", 0), _esc(head), _f(cx), _f(ye), _f(cx), _f(ye + 3)))
|
|
439
|
+
return _Layout(max(H.l, B.l), max(H.r, B.r), H.h + B.h + end_h, draw)
|
|
440
|
+
|
|
441
|
+
# --- whole program --------------------------------------------------------------------------
|
|
442
|
+
def decl_lines(self):
|
|
443
|
+
out = []
|
|
444
|
+
for it in self.ast["items"]:
|
|
445
|
+
t = it["type"]
|
|
446
|
+
if t == "extern":
|
|
447
|
+
k, s = "extern", "%s(%s)%s" % (it["name"], _typed_params(it),
|
|
448
|
+
" -> " + it["returns"] if it.get("returns") else "")
|
|
449
|
+
elif t == "event":
|
|
450
|
+
k, s = "event", it["name"] + ("(%s)" % _typed_params(it) if it.get("params") else "")
|
|
451
|
+
elif t == "timer":
|
|
452
|
+
k, s = "timer", it["name"]
|
|
453
|
+
elif t == "const":
|
|
454
|
+
k, s = "const", "%s = %s" % (it["name"], expr_to_str(it["value"]))
|
|
455
|
+
elif t == "var":
|
|
456
|
+
k, s = it.get("vtype") or "int", it["name"] + (" = " + expr_to_str(it["value"]) if it.get("value") else "")
|
|
457
|
+
else:
|
|
458
|
+
continue
|
|
459
|
+
out.append((k, _clip(s, 38), it))
|
|
460
|
+
return out
|
|
461
|
+
|
|
462
|
+
def card(self, x, y, title, rows):
|
|
463
|
+
"""A declarations card: rows of (keyword, text, node)."""
|
|
464
|
+
w = max([_tw(title)] + [_tw(k + " " + t) for k, t, _n in rows]) + 28
|
|
465
|
+
h = len(rows) * 17 + 30
|
|
466
|
+
self.P('<rect class="decl-box" x="%s" y="%s" width="%s" height="%s" rx="6"/>' % (_f(x), _f(y), _f(w), h))
|
|
467
|
+
self.P('<text class="small" x="%s" y="%s">%s</text>' % (_f(x + 10), _f(y + 16), _esc(title)))
|
|
468
|
+
for i, (k, t, n) in enumerate(rows):
|
|
469
|
+
self.P('<g class="nd decl" data-ids="%s" data-line="%s"><text x="%s" y="%s">'
|
|
470
|
+
'<tspan class="decl-k">%s</tspan> <tspan class="decl-n">%s</tspan></text></g>'
|
|
471
|
+
% (n["id"], n.get("line", 0), _f(x + 10), _f(y + 35 + i * 17), _esc(k), _esc(t)))
|
|
472
|
+
return w, h
|
|
473
|
+
|
|
474
|
+
def render_flow(self):
|
|
475
|
+
M = 24
|
|
476
|
+
y = M
|
|
477
|
+
width = 0
|
|
478
|
+
items = self.ast["items"]
|
|
479
|
+
states = [i for i in items if i["type"] == "state"]
|
|
480
|
+
any_state = next((i for i in items if i["type"] == "any_state"), None)
|
|
481
|
+
name = self.ast.get("name")
|
|
482
|
+
title = "flow %s%s" % (name, " v%d" % self.ast["version"] if self.ast.get("version") else "") if name else "program"
|
|
483
|
+
self.P('<text class="sec-title" x="%s" y="%s">%s</text>' % (M, y + 4, _esc(title)))
|
|
484
|
+
y += 18
|
|
485
|
+
x, row_h = M, 0
|
|
486
|
+
decl = self.decl_lines()
|
|
487
|
+
if decl:
|
|
488
|
+
w, h = self.card(x, y, "SYSTEM INTERFACE", decl)
|
|
489
|
+
x += w + 40
|
|
490
|
+
row_h = h
|
|
491
|
+
for nt in (i for i in items if i["type"] == "comment"):
|
|
492
|
+
nl = self.note_layout(nt["text"])
|
|
493
|
+
self.note_block(nl, x, y, nt["id"])
|
|
494
|
+
x += nl["w"] + 30
|
|
495
|
+
row_h = max(row_h, nl["h"])
|
|
496
|
+
for f in (i for i in items if i["type"] == "func"):
|
|
497
|
+
R = self.routine(f)
|
|
498
|
+
R.draw(x + R.l, y)
|
|
499
|
+
x += R.l + R.r + 40
|
|
500
|
+
row_h = max(row_h, R.h)
|
|
501
|
+
width = max(width, x)
|
|
502
|
+
y += row_h + 36
|
|
503
|
+
|
|
504
|
+
start = next((s for s in states if s.get("initial")), states[0] if states else None)
|
|
505
|
+
containers = ([(True, any_state)] if any_state else []) + [(False, s) for s in states]
|
|
506
|
+
for is_any, st in containers:
|
|
507
|
+
handlers = [self.routine(h) for h in st["handlers"] if h["type"] == "on"]
|
|
508
|
+
svars = [v for v in st.get("vars") or [] if v["type"] == "var"]
|
|
509
|
+
notes = [h for h in (st.get("vars") or []) + st["handlers"] if h["type"] == "comment"]
|
|
510
|
+
top, pad = y, 18
|
|
511
|
+
sx, sh = M + pad, 0
|
|
512
|
+
cur = not is_any and self.cur_state == st["name"]
|
|
513
|
+
title_y = top + 22
|
|
514
|
+
box_at = len(self.out)
|
|
515
|
+
self.P("")
|
|
516
|
+
if is_any:
|
|
517
|
+
self.P('<text class="state-title any-title" x="%s" y="%s">any_state</text>'
|
|
518
|
+
'<text class="small" x="%s" y="%s">handlers for every state · a state\'s own '
|
|
519
|
+
'handler for the same event takes precedence</text>'
|
|
520
|
+
% (M + pad, title_y, _f(M + pad + _tw("any_state") * 1.25 + 18), title_y))
|
|
521
|
+
else:
|
|
522
|
+
is_start = st is start
|
|
523
|
+
self.P('<text class="state-title" x="%s" y="%s">%sstate %s%s</text>'
|
|
524
|
+
% (M + pad + (18 if is_start else 0), title_y, "initial " if st.get("initial") else "",
|
|
525
|
+
_esc(st["name"]), " ◀ current" if cur else ""))
|
|
526
|
+
if is_start:
|
|
527
|
+
self.P('<circle class="initial" cx="%s" cy="%s" r="5"><title>initial state</title></circle>'
|
|
528
|
+
% (M + pad + 5, title_y - 5))
|
|
529
|
+
iy = top + 36
|
|
530
|
+
if svars:
|
|
531
|
+
rows = [(v.get("vtype") or "int",
|
|
532
|
+
v["name"] + (" = " + expr_to_str(v["value"]) if v.get("value") else ""), v)
|
|
533
|
+
for v in svars]
|
|
534
|
+
w, h = self.card(sx, iy, "state variables", rows)
|
|
535
|
+
sx += w + 30
|
|
536
|
+
sh = max(sh, h)
|
|
537
|
+
for nt in notes:
|
|
538
|
+
nl = self.note_layout(nt["text"])
|
|
539
|
+
self.note_block(nl, sx, iy, nt["id"])
|
|
540
|
+
sx += nl["w"] + 30
|
|
541
|
+
sh = max(sh, nl["h"])
|
|
542
|
+
for R in handlers:
|
|
543
|
+
R.draw(sx + R.l, iy)
|
|
544
|
+
sx += R.l + R.r + 36
|
|
545
|
+
sh = max(sh, R.h)
|
|
546
|
+
bw = max(sx - M - 36 + pad + 18, 560 if is_any else 260)
|
|
547
|
+
bh = sh + 36 + 18
|
|
548
|
+
if is_any:
|
|
549
|
+
self.out[box_at] = ('<rect class="state-box any-box" x="%s" y="%s" width="%s" height="%s" rx="12"/>'
|
|
550
|
+
% (M, top, _f(bw), _f(bh)))
|
|
551
|
+
else:
|
|
552
|
+
self.out[box_at] = ('<g class="state%s" data-state="%s"><rect class="state-box" x="%s" y="%s" '
|
|
553
|
+
'width="%s" height="%s" rx="12"/></g>'
|
|
554
|
+
% (" cur" if cur else "", _esc(st["name"]), M, top, _f(bw), _f(bh)))
|
|
555
|
+
width = max(width, M + bw)
|
|
556
|
+
y += bh + 26
|
|
557
|
+
return math.ceil(width + M), math.ceil(y + M)
|
|
558
|
+
|
|
559
|
+
def render_states(self):
|
|
560
|
+
items = self.ast["items"]
|
|
561
|
+
states = [i for i in items if i["type"] == "state"]
|
|
562
|
+
if not states:
|
|
563
|
+
self.P('<text class="small" x="20" y="40">This program has no states.</text>')
|
|
564
|
+
return 420, 80
|
|
565
|
+
n = len(states)
|
|
566
|
+
RAD = 58
|
|
567
|
+
ring = 0 if n <= 1 else max(150, n * (RAD * 2 + 70) / (2 * math.pi))
|
|
568
|
+
cx0, cy0 = ring + RAD + 150, ring + RAD + 70
|
|
569
|
+
pos = {}
|
|
570
|
+
for i, s in enumerate(states):
|
|
571
|
+
a = -math.pi / 2 + i * 2 * math.pi / n + (math.pi / 2 if n == 2 else 0)
|
|
572
|
+
pos[s["name"]] = (cx0 + ring * math.cos(a), cy0 + ring * math.sin(a))
|
|
573
|
+
anyp = (70, 40)
|
|
574
|
+
edges = {}
|
|
575
|
+
|
|
576
|
+
def gotos(stmts, acc):
|
|
577
|
+
for s in stmts or []:
|
|
578
|
+
if s["type"] == "goto":
|
|
579
|
+
acc.append(s)
|
|
580
|
+
if s["type"] == "if":
|
|
581
|
+
for b in s["branches"]:
|
|
582
|
+
gotos(b["body"], acc)
|
|
583
|
+
gotos(s.get("else_body"), acc)
|
|
584
|
+
if s["type"] == "while":
|
|
585
|
+
gotos(s["body"], acc)
|
|
586
|
+
return acc
|
|
587
|
+
|
|
588
|
+
def add(frm, to, label):
|
|
589
|
+
edges.setdefault((frm, to), []).append(label)
|
|
590
|
+
|
|
591
|
+
any_state = next((i for i in items if i["type"] == "any_state"), None)
|
|
592
|
+
any_handlers = [h for h in any_state["handlers"] if h["type"] == "on"] if any_state else []
|
|
593
|
+
W, H = math.ceil(cx0 * 2 + 40), math.ceil(cy0 + ring + RAD + 60)
|
|
594
|
+
if any_state:
|
|
595
|
+
self.P('<rect class="any-frame" x="8" y="12" width="%s" height="%s" rx="16">'
|
|
596
|
+
'<title>any_state: its handlers apply in every state</title></rect>' % (W - 16, H - 20))
|
|
597
|
+
for st in states:
|
|
598
|
+
for h in st["handlers"]:
|
|
599
|
+
if h["type"] == "on":
|
|
600
|
+
for g in gotos(h["body"], []):
|
|
601
|
+
add(st["name"], g["state"], h["event"])
|
|
602
|
+
for h in any_handlers:
|
|
603
|
+
for g in gotos(h["body"], []):
|
|
604
|
+
add("*", g["state"], h["event"])
|
|
605
|
+
pt = lambda name: anyp if name == "*" else pos.get(name)
|
|
606
|
+
arr = self.marker("arr2")
|
|
607
|
+
for (frm, to), labels in edges.items():
|
|
608
|
+
a, b = pt(frm), pt(to)
|
|
609
|
+
if not a or not b:
|
|
610
|
+
continue
|
|
611
|
+
label = _esc(_clip(", ".join(dict.fromkeys(labels)), 30))
|
|
612
|
+
if frm == to:
|
|
613
|
+
ax, ay = a
|
|
614
|
+
self.P('<path class="sd-edge" d="M%s %s C%s %s %s %s %s %s" marker-end="%s"/>'
|
|
615
|
+
% (_f(ax - 20), _f(ay - RAD + 4), _f(ax - 50), _f(ay - RAD - 70), _f(ax + 50),
|
|
616
|
+
_f(ay - RAD - 70), _f(ax + 20), _f(ay - RAD + 4), arr))
|
|
617
|
+
self.P('<text class="sd-label" x="%s" y="%s" text-anchor="middle">%s</text>'
|
|
618
|
+
% (_f(ax), _f(ay - RAD - 56), label))
|
|
619
|
+
continue
|
|
620
|
+
dx, dy = b[0] - a[0], b[1] - a[1]
|
|
621
|
+
d = math.hypot(dx, dy) or 1
|
|
622
|
+
ux, uy = dx / d, dy / d
|
|
623
|
+
nx, ny = -uy, ux
|
|
624
|
+
r0, r1 = (8 if frm == "*" else RAD), RAD + 2
|
|
625
|
+
sx, sy = a[0] + ux * r0 + nx * 10, a[1] + uy * r0 + ny * 10
|
|
626
|
+
ex, ey = b[0] - ux * r1 + nx * 10, b[1] - uy * r1 + ny * 10
|
|
627
|
+
mx, my = (sx + ex) / 2 + nx * 26, (sy + ey) / 2 + ny * 26
|
|
628
|
+
self.P('<path class="sd-edge" d="M%s %s Q%s %s %s %s" marker-end="%s"/>'
|
|
629
|
+
% (_f(sx), _f(sy), _f(mx), _f(my), _f(ex), _f(ey), arr))
|
|
630
|
+
self.P('<text class="sd-label" x="%s" y="%s" text-anchor="middle">%s</text>' % (_f(mx), _f(my), label))
|
|
631
|
+
if any_state:
|
|
632
|
+
self.P('<rect class="any-tag" x="%s" y="%s" width="100" height="28" rx="6"/>'
|
|
633
|
+
'<text class="any-tag-text" x="%s" y="%s" text-anchor="middle">any_state</text>'
|
|
634
|
+
% (anyp[0] - 50, anyp[1] - 14, anyp[0], anyp[1] + 4))
|
|
635
|
+
evs = " · ".join(h["event"] for h in any_handlers)
|
|
636
|
+
if evs:
|
|
637
|
+
self.P('<text class="small" x="%s" y="%s">%s</text>' % (anyp[0] + 60, anyp[1] + 4, _esc(_clip(evs, 60))))
|
|
638
|
+
start = next((s for s in states if s.get("initial")), states[0])
|
|
639
|
+
for s in states:
|
|
640
|
+
x, y = pos[s["name"]]
|
|
641
|
+
cur = self.cur_state == s["name"]
|
|
642
|
+
evs = " · ".join(h["event"] for h in s["handlers"] if h["type"] == "on")
|
|
643
|
+
self.P('<g class="nd sd-state%s" data-state="%s" data-line="%s"><circle class="sh" cx="%s" cy="%s" r="%s"/>'
|
|
644
|
+
'<text class="sd-name" x="%s" y="%s" text-anchor="middle">%s</text>'
|
|
645
|
+
'<text class="small" x="%s" y="%s" text-anchor="middle">%s</text></g>'
|
|
646
|
+
% (" cur" if cur else "", _esc(s["name"]), s.get("line", 0), _f(x), _f(y), RAD,
|
|
647
|
+
_f(x), _f(y - 4), _esc(s["name"]), _f(x), _f(y + 13), _esc(_clip(evs, 18))))
|
|
648
|
+
if s is start:
|
|
649
|
+
self.P('<circle class="initial" cx="%s" cy="%s" r="7"/><path class="sd-edge" d="M%s %s H%s" marker-end="%s"/>'
|
|
650
|
+
% (_f(x - RAD - 46), _f(y), _f(x - RAD - 39), _f(y), _f(x - RAD - 3), arr))
|
|
651
|
+
return W, H
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
# ------------------------------------------------------------------------------------- styles
|
|
655
|
+
_RULES = """
|
|
656
|
+
.fs-chart{background:var(--fs-bg);font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace}
|
|
657
|
+
.fs-chart text{font-size:12px;fill:var(--fs-fg)}
|
|
658
|
+
.fs-chart .small{font-size:10px;fill:var(--fs-muted)}
|
|
659
|
+
.fs-chart .lbl-yes{fill:var(--fs-ok);font-size:10px}.fs-chart .lbl-no{fill:var(--fs-bad);font-size:10px}
|
|
660
|
+
.fs-chart .edge{stroke:var(--fs-edge);stroke-width:1.4;fill:none}.fs-chart .edge.back{stroke-dasharray:4 3}
|
|
661
|
+
.fs-chart .wire{stroke:var(--fs-edge);stroke-width:1.2;fill:none}
|
|
662
|
+
.fs-chart .arrow-head{fill:var(--fs-edge)}
|
|
663
|
+
.fs-chart .nd>.sh{stroke:var(--fs-stroke);stroke-width:1.2}
|
|
664
|
+
.fs-chart .k-proc>.sh{fill:var(--fs-n-proc)}.fs-chart .k-io>.sh{fill:var(--fs-n-io)}
|
|
665
|
+
.fs-chart .k-sub>.sh{fill:var(--fs-n-sub)}.fs-chart .k-dec>.sh{fill:var(--fs-n-dec)}
|
|
666
|
+
.fs-chart .k-goto>.sh{fill:var(--fs-n-goto)}.fs-chart .k-emit>.sh{fill:var(--fs-n-emit)}
|
|
667
|
+
.fs-chart .k-timer>.sh{fill:var(--fs-n-timer)}.fs-chart .k-ret>.sh{fill:var(--fs-n-ret)}
|
|
668
|
+
.fs-chart .k-ev>.sh{fill:var(--fs-n-ev)}.fs-chart .k-end>.sh{fill:var(--fs-n-end)}
|
|
669
|
+
.fs-chart .k-leaf>.sh{fill:var(--fs-n-leaf)}.fs-chart .k-gate>.sh{fill:var(--fs-n-gate)}
|
|
670
|
+
.fs-chart .k-note>.sh{fill:var(--fs-n-note);stroke-dasharray:3 2}
|
|
671
|
+
.fs-chart .k-note text{fill:var(--fs-cmt);font-style:italic}
|
|
672
|
+
.fs-chart .note-line{stroke:var(--fs-cmt);stroke-dasharray:2 3;fill:none}
|
|
673
|
+
.fs-chart .gate-op{font-size:10px;fill:var(--fs-gate-text)}.fs-chart .end{font-size:10px;fill:var(--fs-muted)}
|
|
674
|
+
.fs-chart .sec-title{font-size:13px;font-weight:bold;fill:var(--fs-muted)}
|
|
675
|
+
.fs-chart .decl-box{fill:var(--fs-decl-bg);stroke:var(--fs-line)}
|
|
676
|
+
.fs-chart .decl-k{fill:var(--fs-kw)}.fs-chart .decl-n{fill:var(--fs-fg)}
|
|
677
|
+
.fs-chart .state-box{fill:var(--fs-state-fill);stroke:var(--fs-state);stroke-width:1.5}
|
|
678
|
+
.fs-chart .state.cur .state-box{stroke:var(--fs-cur);stroke-width:2.5;fill:var(--fs-cur-fill)}
|
|
679
|
+
.fs-chart .state-title{font-size:14px;font-weight:bold;fill:var(--fs-state-title)}
|
|
680
|
+
.fs-chart .state.cur+.state-title{fill:var(--fs-cur)}
|
|
681
|
+
.fs-chart .any-box{fill:var(--fs-any-fill);stroke:var(--fs-any);stroke-dasharray:6 4}
|
|
682
|
+
.fs-chart .any-title{fill:var(--fs-any-title)}
|
|
683
|
+
.fs-chart .any-frame{fill:var(--fs-any-fill);stroke:var(--fs-any);stroke-width:1.5;stroke-dasharray:6 4}
|
|
684
|
+
.fs-chart .any-tag{fill:var(--fs-decl-bg);stroke:var(--fs-any)}
|
|
685
|
+
.fs-chart .any-tag-text{font-weight:bold;fill:var(--fs-any-title)}
|
|
686
|
+
.fs-chart .initial{fill:var(--fs-fg)}
|
|
687
|
+
.fs-chart .sd-state>.sh{fill:var(--fs-sd-fill);stroke:var(--fs-sd-stroke);stroke-width:2}
|
|
688
|
+
.fs-chart .sd-state.cur>.sh{fill:var(--fs-cur-fill);stroke:var(--fs-cur);stroke-width:3}
|
|
689
|
+
.fs-chart .sd-name{font-size:13px;font-weight:bold}
|
|
690
|
+
.fs-chart .sd-edge{stroke:var(--fs-edge);stroke-width:1.5;fill:none}
|
|
691
|
+
.fs-chart .sd-label{font-size:11px;fill:var(--fs-ev)}
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _vars(theme):
|
|
696
|
+
return ";".join("--fs-%s:%s" % (k, v) for k, v in THEMES[theme].items())
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _style(theme, cls):
|
|
700
|
+
if theme in THEMES:
|
|
701
|
+
return ".%s{%s}%s" % (cls, _vars(theme), _RULES)
|
|
702
|
+
# auto: dark by default, light when the page prefers it (unless an ancestor says data-theme="dark"),
|
|
703
|
+
# and whatever an ancestor's data-theme says
|
|
704
|
+
return (".%s{%s}" % (cls, _vars("dark")) +
|
|
705
|
+
"@media (prefers-color-scheme:light){:not([data-theme=\"dark\"])>.%s,.%s{%s}"
|
|
706
|
+
"[data-theme=\"dark\"] .%s{%s}}" % (cls, cls, _vars("light"), cls, _vars("dark")) +
|
|
707
|
+
"[data-theme=\"light\"] .%s{%s}[data-theme=\"dark\"] .%s{%s}"
|
|
708
|
+
% (cls, _vars("light"), cls, _vars("dark")) + _RULES)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
# ------------------------------------------------------------------------------------- entry point
|
|
712
|
+
def to_svg(program, view="flow", *, state=None, theme="auto", width=None, id_prefix=None, title=None):
|
|
713
|
+
"""An SVG flowchart (``view="flow"``) or state diagram (``view="states"``) as a string.
|
|
714
|
+
|
|
715
|
+
program: FlowScript source text, a parsed AST (dict) or a ``Program`` from ``flowscript.compile``
|
|
716
|
+
(it keeps its source). A program loaded from bytecode cannot be drawn: the chart is
|
|
717
|
+
built from the source, which bytecode does not contain.
|
|
718
|
+
state: name of the current state, highlighted (e.g. a runner's live state).
|
|
719
|
+
theme: "auto" (follow the page), "dark" or "light".
|
|
720
|
+
width: None: the natural size in pixels; "100%" (or any CSS width): scale to the container.
|
|
721
|
+
id_prefix: prefix for element ids (default: derived from the chart, unique per program/view/state).
|
|
722
|
+
title: accessible title (default: the program's name).
|
|
723
|
+
"""
|
|
724
|
+
ast = _ast_of(program)
|
|
725
|
+
if view not in ("flow", "states"):
|
|
726
|
+
raise ValueError("view must be 'flow' or 'states'")
|
|
727
|
+
if theme not in ("auto", "dark", "light"):
|
|
728
|
+
raise ValueError("theme must be 'auto', 'dark' or 'light'")
|
|
729
|
+
if id_prefix is None:
|
|
730
|
+
key = repr((_strip(ast), view, state)).encode("utf-8")
|
|
731
|
+
id_prefix = "fs" + hashlib.sha1(key).hexdigest()[:8]
|
|
732
|
+
chart = _Chart(ast, id_prefix, state)
|
|
733
|
+
W, H = chart.render_flow() if view == "flow" else chart.render_states()
|
|
734
|
+
cls = "fs-chart"
|
|
735
|
+
label = title or ("flow " + ast["name"] if ast.get("name") else "FlowScript program")
|
|
736
|
+
size = ('width="%s" height="%s"' % (W, H) if width is None else
|
|
737
|
+
'width="%s" style="max-width:%dpx;height:auto"' % (_esc(width), W))
|
|
738
|
+
defs = ('<defs><marker id="{p}-arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" '
|
|
739
|
+
'orient="auto-start-reverse"><path class="arrow-head" d="M0 1 L10 5 L0 9 z"/></marker>'
|
|
740
|
+
'<marker id="{p}-arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" '
|
|
741
|
+
'orient="auto"><path class="arrow-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>').format(p=id_prefix)
|
|
742
|
+
return ('<svg xmlns="http://www.w3.org/2000/svg" class="%s" %s viewBox="0 0 %d %d" role="img" '
|
|
743
|
+
'aria-label="%s" data-view="%s"><title>%s</title><style>%s</style>%s%s</svg>'
|
|
744
|
+
% (cls, size, W, H, _esc(label), view, _esc(label), _style(theme, cls), defs, "".join(chart.out)))
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _strip(ast):
|
|
748
|
+
"""The AST without positions, for a stable id prefix."""
|
|
749
|
+
if isinstance(ast, dict):
|
|
750
|
+
return tuple(sorted((k, _strip(v)) for k, v in ast.items() if k not in ("id", "line")))
|
|
751
|
+
if isinstance(ast, list):
|
|
752
|
+
return tuple(_strip(v) for v in ast)
|
|
753
|
+
return ast
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _ast_of(program):
|
|
757
|
+
from .parser import parse
|
|
758
|
+
if isinstance(program, str):
|
|
759
|
+
return parse(program)
|
|
760
|
+
if isinstance(program, dict) and program.get("type") == "program":
|
|
761
|
+
return program
|
|
762
|
+
ast = getattr(program, "ast", None)
|
|
763
|
+
if ast is not None:
|
|
764
|
+
return ast
|
|
765
|
+
raise TypeError("to_svg needs FlowScript source, an AST or a Program from flowscript.compile() "
|
|
766
|
+
"(bytecode alone has no source to draw)")
|