feat: referentiel projets dynamique + validation nommage by design

Durcit la convention de nommage des projets (dérive constatée : 'Sliding
Automation', 'code_versioning'... au lieu des formes canoniques).

- trilium_api.py : projets_canoniques() lit le référentiel = valeurs du label
  projet sur les notes de type=projet (source unique, pas de constante en dur).
  Note-projet CodeVersioning créée (manquait).
- mcp_server.py : _valider_projet() branché dans les 6 tools de création
  (add_decision/history/backlog, new_conversation, create_entite, add_skill).
  Refuse un projet non canonique (suggestion si faute) ou inconnu (renvoi au
  processus de création de projet). Ne verrouille pas si référentiel illisible.
- lint_audit.py : VAL-nommage aligné sur le référentiel (attrape casse, espace
  ET snake_case ; l'ancien 'contient un espace' ratait code_versioning).
- Données : 79 notes ré-étiquetées vers les 3 formes canoniques.

Quality by design : l'erreur de nommage devient impossible à l'écriture, le
Lint n'est plus que le filet de sécurité.
This commit is contained in:
2026-07-17 14:59:33 +02:00
parent d1e67431b6
commit 943acbc573
2425 changed files with 710525 additions and 6 deletions
@@ -0,0 +1,55 @@
# Copyright wavedrompy contributors.
# SPDX-License-Identifier: MIT
import argparse
import json
import yaml
import sys
from .waveform import WaveDrom
from .assign import Assign
from .version import version
from .bitfield import BitField
import json
def fixQuotes(inputString):
# fix double quotes in the input file. opening with yaml and dumping with json fix the issues.
yamlCode = yaml.load(inputString, Loader=yaml.FullLoader)
fixedString = json.dumps(yamlCode, indent=4)
return fixedString
def render(source="", output=[], strict_js_features = False):
source = json.loads(fixQuotes(source))
if source.get("signal"):
return WaveDrom().render_waveform(0, source, output, strict_js_features)
elif source.get("assign"):
return Assign().render(0, source, output)
elif source.get("reg"):
return BitField().renderJson(source)
def render_write(source, output, strict_js_features = False):
jinput = source.read()
out = render(jinput, strict_js_features=strict_js_features)
out.write(output)
def render_file(source, output, strict_js_features = False):
out = open(output, "w")
render_write(open(source, "r"), out, strict_js_features=strict_js_features)
out.close()
def main():
parser = argparse.ArgumentParser(description="")
parser.add_argument("--input", "-i", help="<input wavedrom source filename>",
required=True, type=argparse.FileType('r'))
parser.add_argument("--svg", "-s", help="<output SVG image file name>",
nargs='?', type=argparse.FileType('w'), default=sys.stdout)
args = parser.parse_args()
render_write(args.input, args.svg, False)
@@ -0,0 +1,174 @@
# Copyright wavedrompy contributors.
# SPDX-License-Identifier: MIT
# Translated to Python from original file:
# https://github.com/drom/wavedrom/blob/master/src/WaveDrom.js
from collections import namedtuple
import svgwrite
from .base import SVGBase
class RenderState:
def __init__(self, x=0, y=0, xmax=0):
self.x = x
self.y = y
self.xmax = xmax
def __str__(self):
return "x={} y={}, xmax={}".format(self.x, self.y, self.xmax)
RenderObject = namedtuple("RenderObject", "name x y")
class Assign(SVGBase):
def render_tree(self, tree, state):
state.xmax = max(state.xmax, state.x)
y = state.y
for i in range(1, len(tree)):
if isinstance(tree[i], list):
state = self.render_tree(tree[i], RenderState(x=state.x+1, y=state.y, xmax=state.xmax))
else:
tree[i] = RenderObject(name=tree[i], x=state.x+1, y=state.y)
state.y += 2
tree[0] = RenderObject(name=tree[0], x=state.x, y=round((y+state.y-2)/2))
state.x -= 1
return state
def draw_body(self, type, ymin, ymax):
circle = ' M 4,0 C 4,1.1 3.1,2 2,2 0.9,2 0,1.1 0,0 c 0,-1.1 0.9,-2 2,-2 1.1,0 2,0.9 2,2 z'
gates = {
'~': 'M -11,-6 -11,6 0,0 z m -5,6 5,0' + circle,
'=': 'M -11,-6 -11,6 0,0 z m -5,6 5,0',
'&': 'm -16,-10 5,0 c 6,0 11,4 11,10 0,6 -5,10 -11,10 l -5,0 z',
'~&': 'm -16,-10 5,0 c 6,0 11,4 11,10 0,6 -5,10 -11,10 l -5,0 z' + circle,
'|': 'm -18,-10 4,0 c 6,0 12,5 14,10 -2,5 -8,10 -14,10 l -4,0 c 2.5,-5 2.5,-15 0,-20 z',
'~|': 'm -18,-10 4,0 c 6,0 12,5 14,10 -2,5 -8,10 -14,10 l -4,0 c 2.5,-5 2.5,-15 0,-20 z' + circle,
'^': 'm -21,-10 c 1,3 2,6 2,10 m 0,0 c 0,4 -1,7 -2,10 m 3,-20 4,0 c 6,0 12,5 14,10 -2,5 -8,10 -14,10 l -4,0 c 1,-3 2,-6 2,-10 0,-4 -1,-7 -2,-10 z',
'~^': 'm -21,-10 c 1,3 2,6 2,10 m 0,0 c 0,4 -1,7 -2,10 m 3,-20 4,0 c 6,0 12,5 14,10 -2,5 -8,10 -14,10 l -4,0 c 1,-3 2,-6 2,-10 0,-4 -1,-7 -2,-10 z' + circle,
'+': 'm -8,5 0,-10 m -5,5 10,0 m 3,0 c 0,4.418278 -3.581722,8 -8,8 -4.418278,0 -8,-3.581722 -8,-8 0,-4.418278 3.581722,-8 8,-8 4.418278,0 8,3.581722 8,8 z',
'*': 'm -4,4 -8,-8 m 0,8 8,-8 m 4,4 c 0,4.418278 -3.581722,8 -8,8 -4.418278,0 -8,-3.581722 -8,-8 0,-4.418278 3.581722,-8 8,-8 4.418278,0 8,3.581722 8,8 z'
}
iec = {
"BUF": 1, "INV": 1, "AND": '&', "NAND": '&',
"OR": '\u22651', "NOR": '\u22651', "XOR": '=1', "XNOR": '=1', "box": ''
}
circled = { "INV", "NAND", "NOR", "XNOR" }
if ymax == ymin:
ymax = 4
ymin = -4
if type in gates:
return self.element.path(class_='gate', d=gates[type])
elif type in iec:
g = self.container.g()
if type in circled:
path = self.element.path(class_="gate", d="m -16,{} 16,0 0,{} -16,0 z {}".format(ymin-3, ymax-ymin+6, circle))
else:
path = self.element.path(class_="gate", d="m -16,{} 16,0 0,{} -16,0 z".format(ymin-3, ymax-ymin+6))
g.add(path)
tspan = self.element.tspan(iec[type], x=[-14], y=[4], class_='wirename')
text = self.element.text('')
text.add(tspan)
g.add(text)
return g
else:
tspan = self.element.tspan(type, x=[-14], y=[4], class_='wirename')
text = self.element.text('')
text.add(tspan)
return text
def draw_gate(self, spec):
ret = self.container.g()
ys = [s[1] for s in spec[2:]]
ymin = min(ys)
ymax = max(ys)
g = self.container.g(transform="translate(16,0)")
g.add(self.element.path(d="M {},{} {},{}".format(spec[2][0], ymin, spec[2][0], ymax), class_='wire'))
ret.add(g)
for s in spec[2:]:
path = self.element.path(d="m {},{} 16,0".format(s[0], s[1]), class_='wire')
ret.add(self.container.g().add(path))
g = self.container.g(transform="translate({},{})".format(spec[1][0], spec[1][1]))
g.add(self.element.title(spec[0]))
g.add(self.draw_body(spec[0], ymin - spec[1][1], ymax - spec[1][1]))
ret.add(g)
return ret
def draw_boxes(self, tree, xmax):
ret = self.container.g()
spec = []
if isinstance(tree, list):
spec.append(tree[0].name);
spec.append([32 * (xmax - tree[0].x), 8 * tree[0].y]);
for t in tree[1:]:
if isinstance(t, list):
spec.append([32 * (xmax - t[0].x), 8 * t[0].y])
else:
spec.append([32 * (xmax - t.x), 8 * t.y])
ret.add(self.draw_gate(spec))
for t in tree[1:]:
ret.add(self.draw_boxes(t, xmax))
else:
fname = tree.name
fx = 32 * (xmax - tree.x)
fy = 8 * tree.y
g = self.container.g(transform="translate({},{})".format(fx, fy))
g.add(self.element.title(fname))
g.add(self.element.path(d='M 2,0 a 2,2 0 1 1 -4,0 2,2 0 1 1 4,0 z'))
tspan = self.element.tspan(fname, x=[-4], y=[4], class_='pinname')
text = self.element.text('')
text.add(tspan)
g.add(text)
ret.add(g)
return ret
def render(self, index = 0, source = {}, output = []):
STYLE = ".pinname {font-size:12px; font-style:normal; font-variant:normal; font-weight:500; font-stretch:normal; text-align:center; text-anchor:end; font-family:Helvetica} .wirename {font-size:12px; font-style:normal; font-variant:normal; font-weight:500; font-stretch:normal; text-align:center; text-anchor:start; font-family:Helvetica} .wirename:hover {fill:blue} .gate {color:#000; fill:#ffc; fill-opacity: 1;stroke:#000; stroke-width:1; stroke-opacity:1} .gate:hover {fill:red !important; } .wire {fill:none; stroke:#000; stroke-width:1; stroke-opacity:1} .grid {fill:#fff; fill-opacity:1; stroke:none}"
tree = source.get("assign")
state = RenderState(x=0, y=2, xmax=0)
for t in tree:
state = self.render_tree(t, state)
state.x += 1
xmax = state.xmax + 3
width = 32 * (xmax + 1) + 1
height = 8 * (state.y + 1) - 7
ilen = 4 * (xmax + 1)
jlen = state.y + 1
grid = self.container.g()
for i in range(ilen+1):
for j in range(jlen+1):
grid.add(self.element.rect(height=1, width=1, x=(i * 8 - 0.5), y=(j * 8 - 0.5), class_='grid'))
for t in tree:
content = self.draw_boxes(t, xmax)
attr = { 'viewBox': "0 0 {} {}".format(width, height)}
template = svgwrite.Drawing(id="svgcontent_{index}".format(index=index), size=[width, height], **attr)
template.defs.add(svgwrite.container.Style(content=STYLE))
g = self.container.g(transform="translate(0.5,0.5)")
g.add(grid)
g.add(content)
template.add(g)
return template
@@ -0,0 +1,4 @@
class AttrDict(dict):
def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__ = self
@@ -0,0 +1,21 @@
# Copyright wavedrompy contributors.
# SPDX-License-Identifier: MIT
import svgwrite
from .attrdict import AttrDict
class SVGBase(object):
container = AttrDict({
"defs": svgwrite.container.Defs,
"g": svgwrite.container.Group,
"marker": svgwrite.container.Marker,
"use": svgwrite.container.Use,
})
element = AttrDict({
"rect": svgwrite.shapes.Rect,
"path": svgwrite.path.Path,
"text": svgwrite.text.Text,
"tspan": svgwrite.text.TSpan,
"title": svgwrite.base.Title,
"line": svgwrite.shapes.Line,
})
@@ -0,0 +1,259 @@
# Copyright wavedrompy contributors.
# SPDX-License-Identifier: MIT
# Translated to Python from original file:
# https://github.com/drom/wavedrom/blob/master/src/WaveDrom.js
from math import floor
import svgwrite
from .base import SVGBase
from .tspan import TspanParser
class Options:
def __init__(self, vspace=80, hspace=800, lanes=1, bits=32, hflip=False, vflip=False, fontsize=14, fontfamily='sans-serif', fontweight='normal'):
self.vspace = vspace if vspace > 19 else 80
self.hspace = hspace if hspace > 39 else 800
self.lanes = lanes if lanes > 0 else 1
self.bits = bits if bits > 4 else 32
self.hflip = hflip
self.vflip = vflip
self.fontsize = fontsize if fontsize > 5 else 14
self.fontfamily = fontfamily
self.fontweight = fontweight
colors = {2: 0, 3: 80, 4: 170, 5: 45, 6: 126, 7: 215}
def type_style(t):
if t in colors.keys():
return ";fill:hsl({},100%,50%)".format(colors[t])
else:
return ''
class BitField(SVGBase):
def tspan_parse(self, text):
parser = TspanParser()
parser.feed(text)
return parser.get_text()
def hline(self, len, x=0, y=0):
return self.element.line(start=(x,y), end=(x+len,y))
def vline(self, len, x=0, y=0):
return self.element.line(start=(x,y), end=(x,y+len))
def get_text(self, body, x, y=None):
x_list = None
if x:
x_list = [x]
y_list = None
if y:
y_list = [y]
text = self.element.text('', x=x_list, y=y_list)
for t in self.tspan_parse(str(body)):
text.add(t)
return text
def get_label(self, attr, x, y, step=0, length=0):
if isinstance(attr, int):
attr = int(attr)
res = []
for i in range(length):
val = (attr >> i) & 1
xi = x + step * (length / 2 - i - 0.5)
res.append(self.get_text(val, xi, y))
return res
else:
if '\n' in attr:
names = attr.split('\n')
count = len(names)
return [
self.get_text(name, x, y + (-(count - 1) / 2 + i) * self.opt.fontsize)
for (i, name) in enumerate(names)
]
return [self.get_text(attr, x, y)]
def get_attrs(self, e, step, lsbm, msbm):
if self.opt.vflip:
x = step * (msbm + lsbm) / 2
else:
x = step * (self.mod - ((msbm + lsbm) / 2) - 1)
attr = e['attr']
bits = e['bits']
attrs = [attr]
# 'attr' supports both a scalar and a list.
if isinstance(attr, list):
attrs = attr
return [self.get_label(a, x, 16 * i, step, bits)
for (i, a) in enumerate(attrs)]
def labelArr(self, desc):
step = self.opt.hspace / self.mod
bits = self.container.g(transform="translate({},{})".format(step/2, self.opt.vspace/5))
names = self.container.g(transform="translate({},{})".format(step/2, self.opt.vspace/2+4))
attrs = self.container.g(transform="translate({},{})".format(step/2, self.opt.vspace))
blanks = self.container.g(transform="translate(0,{})".format(self.opt.vspace/4))
for e in desc:
lsbm = 0
msbm = self.mod - 1
lsb = self.index * self.mod
msb = (self.index + 1) * self.mod - 1
if floor(e["lsb"] / self.mod) == self.index:
lsbm = e["lsbm"]
lsb = e["lsb"]
if floor(e["msb"] / self.mod) == self.index:
msb = e["msb"]
msbm = e["msbm"]
else:
if floor(e["msb"] / self.mod) == self.index:
msb = e["msb"]
msbm = e["msbm"]
else:
continue
if self.opt.vflip:
bits.add(self.get_text(lsb, x=[step*lsbm]))
else:
bits.add(self.get_text(lsb, x=[step*(self.mod-lsbm - 1)]))
if lsbm != msbm:
if self.opt.vflip:
bits.add(self.get_text(msb, x=[step * msbm]))
else:
bits.add(self.get_text(msb, x=[step * (self.mod - msbm - 1)]))
if e.get('name'):
if self.opt.vflip:
x = step*(msbm+lsbm)/2
else:
x = step*(self.mod-((msbm+lsbm)/2)-1)
for n in self.get_label(e['name'], x, 0):
names.add(n)
if not e.get('name') or e.get('type'):
style = 'fill-opacity:0.1' + type_style(e.get('type', 0))
if self.opt.vflip:
insert_x = lsbm
else:
insert_x = self.mod - msbm - 1
insert = [step * insert_x, 0]
size = [step * (msbm - lsbm + 1), self.opt.vspace/2]
blanks.add(self.element.rect(insert=insert, size=size, style=style))
if e.get('attr') is not None:
for attr in self.get_attrs(e, step, lsbm, msbm):
for a in attr:
attrs.add(a)
g = self.container.g()
g.add(blanks)
g.add(bits)
g.add(names)
g.add(attrs)
return g
def labels(self, desc):
g = self.container.g(text_anchor='middle')
g.add(self.labelArr(desc))
return g
def cage(self, desc):
hspace = self.opt.hspace
vspace = self.opt.vspace
mod = self.mod
g = self.container.g(stroke='black', stroke_width=1, stroke_linecap='round', transform="translate(0,{})".format(vspace/4))
g.add(self.hline(hspace));
if self.opt.vflip:
g.add(self.vline(0));
else:
g.add(self.vline(vspace / 2));
g.add(self.hline(hspace, 0, vspace / 2));
i = self.index * mod
if self.opt.vflip:
r = range(0, mod + 1)
else:
r = range(mod, 0, -1)
for j in r:
if j == mod or any([(e["lsb"] == i) for e in desc]):
g.add(self.vline((vspace / 2), j * (hspace / mod)));
else:
g.add(self.vline((vspace / 16), j * (hspace / mod)));
g.add(self.vline((vspace / 16), j * (hspace / mod), vspace * 7 / 16));
i += 1
return g
def lane(self, desc):
x = 4.5
if self.opt.hflip:
i = self.index
else:
i = self.opt.lanes-self.index-1
y = i * self.opt.vspace + 0.5
g = self.container.g(transform = "translate({},{})".format(x, y),
text_anchor = "middle",
font_size = self.opt.fontsize,
font_family = self.opt.fontfamily,
font_weight = self.opt.fontweight)
g.add(self.cage(desc))
g.add(self.labels(desc))
return g
def get_max_attrs(self, desc):
max_count = 0
for e in desc:
if 'attr' in e:
if isinstance(e['attr'], list):
max_count = max(max_count, len(e['attr']))
else:
max_count = max(max_count, 1)
return max_count
def render(self, desc, opt = Options()):
self.opt = opt
# Compute extra per-lane space needed if there are more than one attr
# for any field. This spaces all lanes uniformly, matching the lane
# with the most attr's.
extra_attrs = 0
max_attrs = self.get_max_attrs(desc)
if max_attrs > 1:
extra_attrs = max_attrs - 1
self.extra_attr_space = extra_attrs * 16
width = opt.hspace + 9
height = (opt.vspace + self.extra_attr_space) * opt.lanes + 5
template = svgwrite.Drawing()
template["width"] = width
template["height"] = height
template["class"] = "WaveDrom"
template.viewbox(0, 0, width, height)
lsb = 0
self.mod = int(opt.bits / opt.lanes)
for e in desc:
e["lsb"] = lsb
e["lsbm"] = lsb % self.mod
lsb += e['bits']
e['msb'] = lsb - 1
e['msbm'] = e['msb'] % self.mod
for i in range(opt.lanes):
self.index = i
template.add(self.lane(desc))
return template
def renderJson(self, source):
opt = Options()
if source.get("config"):
opt = Options(**source['config'])
if source.get("reg"):
return self.render(source['reg'], opt)
@@ -0,0 +1,421 @@
# Copyright wavedrompy contributors.
# SPDX-License-Identifier: MIT
# Translated to Python from original file:
# https://github.com/drom/wavedrom/blob/master/src/WaveDrom.js
from .attrdict import AttrDict
css = AttrDict({})
css.default = """
text{font-size:11pt;
font-style:normal;
font-variant:normal;
font-weight:normal;
font-stretch:normal;
text-align:center;
fill-opacity:1;
font-family:Helvetica}
.h1{font-size:33pt;
font-weight:bold}
.h2{font-size:27pt;
font-weight:bold}
.h3{font-size:20pt;
font-weight:bold}
.h4{font-size:14pt;
font-weight:bold}
.h5{font-size:11pt;
font-weight:bold}
.h6{font-size:8pt;
font-weight:bold}
.muted{fill:#aaa}
.warning{fill:#f6b900}
.error{fill:#f60000}
.info{fill:#0041c4}
.success{fill:#00ab00}
.s1{fill:none;
stroke:#000;
stroke-width:1;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none}
.s2{fill:none;
stroke:#000;
stroke-width:0.5;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none}
.s3{color:#000;
fill:none;
stroke:#000;
stroke-width:1;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:1, 3;
stroke-dashoffset:0;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s4{color:#000;
fill:none;
stroke:#000;
stroke-width:1;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none;
stroke-dashoffset:0;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s5{fill:#fff;
stroke:none}
.s6{fill:#000;
fill-opacity:1;
stroke:none}
.s7{color:#000;
fill:#fff;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s8{color:#000;
fill:#ffffb4;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s9{color:#000;
fill:#ffe0b9;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s10{color:#000;
fill:#b9e0ff;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s11{color:#000;
fill:#ccfdfe;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s12{color:#000;
fill:#cdfdc5;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s13{color:#000;
fill:#f0c1fb;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s14{color:#000;
fill:#f5c2c0;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s15{fill:#0041c4;
fill-opacity:1;
stroke:none}
.s16{fill:none;
stroke:#0041c4;
stroke-width:1;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none}
"""
css.narrow = """
text{font-size:11pt;
font-style:normal;
font-variant:normal;
font-weight:normal;
font-stretch:normal;
text-align:center;
fill-opacity:1;
font-family:Helvetica}
.muted{fill:#aaa}
.warning{fill:#f6b900}
.error{fill:#f60000}
.info{fill:#0041c4}
.success{fill:#00ab00}
.h1{font-size:33pt;font-weight:bold}
.h2{font-size:27pt;font-weight:bold}
.h3{font-size:20pt;font-weight:bold}
.h4{font-size:14pt;font-weight:bold}
.h5{font-size:11pt;font-weight:bold}
.h6{font-size:8pt;font-weight:bold}
.s1{fill:none;
stroke:#000000;
stroke-width:1;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none}
.s2{fill:none;
stroke:#000000;
stroke-width:0.5;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none}
.s3{color:#000000;
fill:none;
stroke:#000000;
stroke-width:1;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:1, 3;
stroke-dashoffset:0;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s4{color:#000000;
fill:none;
stroke:#000000;
stroke-width:1;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none;
stroke-dashoffset:0;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s5{fill:#ffffff;stroke:none}
.s6{color:#000000;
fill:#ffffb4;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s7{color:#000000;
fill:#ffe0b9;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s8{color:#000000;
fill:#b9e0ff;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s9{fill:#000000;fill-opacity:1;stroke:none}
.s10{color:#000000;
fill:#ffffff;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:1px;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
"""
css.lowkey="""
text{font-size:11pt;
font-style:normal;
font-variant:normal;
font-weight:normal;
font-stretch:normal;
text-align:center;
fill-opacity:1;
font-family:Helvetica}
.muted{fill:#aaa}
.warning{fill:#f6b900}
.error{fill:#f60000}
.info{fill:#0041c4}
.success{fill:#00ab00}
.h1{font-size:33pt;
font-weight:bold}
.h2{font-size:27pt;
font-weight:bold}
.h3{font-size:20pt;
font-weight:bold}
.h4{font-size:14pt;
font-weight:bold}
.h5{font-size:11pt;
font-weight:bold}
.h6{font-size:8pt;
font-weight:bold}
.s1{fill:none;
stroke:#606060;
stroke-width:0.75px;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none}
.s2{fill:none;
stroke:#606060;
stroke-width:0.5;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none}
.s3{color:#000;
fill:none;
stroke:#606060;
stroke-width:0.75px;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:1, 3;
stroke-dashoffset:0;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s4{color:#000;
fill:none;
stroke:#606060;
stroke-width:0.75px;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none;
stroke-dashoffset:0;
marker:none;
visibility:visible;
display:inline;
overflow:visible}
.s5{fill:#ffffff;
stroke:none}
.s6{color:#000;
fill:#eaeaea;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:0.25px;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s7{color:#000;
fill:#d7d7d7;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:0.25px;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s8{color:#000;
fill:#c0c0c0;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:0.25px;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s9{fill:#606060;
fill-opacity:1;
stroke:none}
.s10{color:#000;
fill:#fff;
fill-opacity:1;
fill-rule:nonzero;
stroke:none;
stroke-width:0.25px;
marker:none;
visibility:visible;
display:inline;
overflow:visible;
enable-background:accumulate}
.s11{fill:#0041c4;
fill-opacity:1;
stroke:none}
.s12{fill:none;
stroke:#0041c4;
stroke-width:0.75px;
stroke-linecap:round;
stroke-linejoin:miter;
stroke-miterlimit:4;
stroke-opacity:1;
stroke-dasharray:none}
"""
@@ -0,0 +1,140 @@
# Copyright wavedrompy contributors.
# SPDX-License-Identifier: MIT
import sys
import svgwrite
from six import string_types
from svgwrite.base import BaseElement
from svgwrite.etree import etree
from .attrdict import AttrDict
if sys.version_info < (3, 0):
from HTMLParser import HTMLParser
else:
from html.parser import HTMLParser
class TspanParser(HTMLParser, object):
tags = {
'o': {'text_decoration': 'overline'},
'ins': {'text_decoration': 'underline'},
'sub': {'baseline_shift': 'sub'},
'sup': {'baseline_shift': 'super'},
'b': {'font_weight': 'bold'},
'i': {'font_style': 'italic'},
's': {'text_decoration': 'line-through'},
'tt': {'font_family': 'monospace'},
}
def __init__(self):
super(TspanParser, self).__init__()
self.text = []
self.state = []
def handle_starttag(self, tag, attrs):
self.state.append(tag)
def handle_endtag(self, tag):
if self.state.pop() != tag:
raise RuntimeError("Unexpected closing tag: {}".format(tag))
def get_style(self):
return {k: v for d in [self.tags[t] for t in self.state] for k, v in d.items()}
def handle_data(self, data):
if len(self.state) == 0:
self.text.append(svgwrite.text.TSpan(data))
else:
self.text.append(svgwrite.text.TSpan(data, **self.get_style()))
def get_text(self):
return self.text
class JsonMLElement(BaseElement):
"""Class that generates xml elements from jsonml."""
def __init__(self, source, **extra):
"""Constructs from jsonml source."""
self._jsonml = self.extract_element(source)
self.elementname = self._jsonml.tagname
self._jsonml.attributes.update(extra)
super(JsonMLElement, self).__init__(**extra)
def extract_element(self, e):
"""Extract AttrDict from jsonml
This function non-recursively extracts an AttrDict from jsonml.
This AttrDict has the three elements tagname, attributes and
element_list according to the jsonml specification.
:param e: element as jsonml list/tuple
:return: AttrDict
"""
if not isinstance(e, (list, tuple)):
raise ValueError("JsonML must be a list")
if len(e) == 0:
raise ValueError("JsonML cannot be an empty list")
if not isinstance(e[0], string_types):
raise ValueError("JsonML tagname must be string")
ret = AttrDict({"tagname": e[0], "attributes": {}, "element-list": []})
if len(e) > 1:
if isinstance(e[1], dict):
ret.attributes = e[1]
if len(e) > 2:
ret.element_list = e[2:]
else:
ret.element_list = e[1:]
return ret
def get_xml_element(self, e):
"""Generate xml element from jsonml AttrDict
Recursively generates xml element from jsonml AttrDict.
:param e: jsonml AttrDict
:return: xml element
"""
# create element
element = etree.Element(e.tagname)
# set element attributes
for attribute, value in sorted(e.attributes.items()):
# filter 'None' values
if value is not None:
value = self.value_to_string(value)
if value: # just add not empty attributes
element.set(attribute, value)
# store the last xml sibling, because we may need to add
# text to it's tail. This is to support the tagged text
# style ("<tspan>a<tspan>b</tspan>c</tspan>")
last = None
for c in e.element_list:
if isinstance(c, string_types):
# Strings need special treatment for insertion
# as those are not elements
if last is None:
# No non-text element seen so far
if element.text is None:
# No other element seen so far
element.text = c
else:
# Append to other texts
element.text += c
else:
# There was an element already
if last.tail is None:
# No text after that so far
last.tail = c
else:
# Append to other text
last.tail += c
else:
# Recurse
last = self.get_xml_element(self.extract_element(c))
element.append(last)
return element
def get_xml(self):
return self.get_xml_element(self._jsonml)
@@ -0,0 +1,34 @@
# file generated by setuptools-scm
# don't change, don't track in version control
__all__ = [
"__version__",
"__version_tuple__",
"version",
"version_tuple",
"__commit_id__",
"commit_id",
]
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Tuple
from typing import Union
VERSION_TUPLE = Tuple[Union[int, str], ...]
COMMIT_ID = Union[str, None]
else:
VERSION_TUPLE = object
COMMIT_ID = object
version: str
__version__: str
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE
commit_id: COMMIT_ID
__commit_id__: COMMIT_ID
__version__ = version = '2.0.3.post3'
__version_tuple__ = version_tuple = (2, 0, 3, 'post3')
__commit_id__ = commit_id = None
@@ -0,0 +1,925 @@
# Copyright wavedrompy contributors.
# SPDX-License-Identifier: MIT
# Originally translated to Python from original file:
# https://github.com/drom/wavedrom/blob/master/src/WaveDrom.js
# Now many parts have been rewritten and diverged
import sys
import math
import re
from itertools import chain
import svgwrite
from .attrdict import AttrDict
from collections import deque
from six import string_types
from wavedrom.tspan import JsonMLElement
from . import waveskin, css
from .base import SVGBase
class WaveDrom(SVGBase):
def __init__(self):
self.font_width = 7
self.lane = AttrDict({
"xs": 20, # tmpgraphlane0.width
"ys": 20, # tmpgraphlane0.height
"xg": 120, # tmpgraphlane0.x
"yg": 0, # head gap
"yh0": 0, # head gap title
"yh1": 0, # head gap
"yf0": 0, # foot gap
"yf1": 0, # foot gap
"y0": 5, # tmpgraphlane0.y
"yo": 30, # tmpgraphlane1.y - y0
"tgo": -10, # tmptextlane0.x - xg
"ym": 15, # tmptextlane0.y - y0
"xlabel": 6, # tmptextlabel.x - xg
"xmax": 1,
"scale": 1,
"head": {},
"foot": {}
})
@staticmethod
def stretch_bricks(wave, stretch):
stretcher = {
"Pclk": "111", "Nclk": "000",
"pclk": "111", "nclk": "000",
"0": "000", "1": "111", "x": "xxx", "d": "ddd", "u": "uuu", "z": "zzz",
"2": "vvv-2", "3": "vvv-3", "4": "vvv-4", "5": "vvv-5", "6": "vvv-6",
"7": "vvv-7", "8": "vvv-8", "9": "vvv-9"}
if stretch == -0.5:
# This is the only valid non-integer value, it essentially means halfing down. Further subsampling
# does not work I think..
return wave[0::2]
else:
stretch = int(stretch)
def getBrick(w):
if w in stretcher:
return stretcher[w]
elif w[2] in stretcher:
return stretcher[w[2]]
else:
return stretcher[w[-1]]
if stretch > 0:
return list(chain.from_iterable(([w] + [getBrick(w)]*stretch for w in wave)))
else:
return wave
def gen_wave_brick(self, prev=None, this=None, stretch=0, repeat=0, subcycle=False):
sharpedge_clk = { "p": "pclk", "n": "nclk", "P": "Pclk", "N": "Nclk" }
sharpedge_sig = { "h": "pclk", "l": "nclk", "H": "Pclk", "L": "Nclk" }
sharpedge = sharpedge_clk.copy()
sharpedge.update(sharpedge_sig)
# level: logical levels of symbols at wave
level = {"=": "v", "2": "v", "3": "v", "4": "v", "5": "v", "6": "v",
"7": "v", "8": "v", "9": "v", "h": "1", "H": "1", "l": "0", "L": "0"}
# translevel: Those are the levels at the end of a cycle (special for clocks)
translevel = level.copy()
translevel.update({"p": "0", "P": "0", "n": "1", "N": "1"})
# data: Modifiers of wavebricks that add data
data = {"=": "-2", "2": "-2", "3": "-3", "4": "-4", "5": "-5", "6":
"-6", "7": "-7", "8": "-8", "9": "-9"}
# clkinvert: The inverse brick to clock symbols
clkinvert = {"p": "nclk", "n": "pclk", "P": "nclk", "N": "pclk"}
# xclude: Those are actually identical levels, no transition
xclude = {"hp": "111", "Hp": "111", "ln": "000", "Ln": "000",
"nh": "111", "Nh": "111", "pl": "000", "Pl": "000"}
if this in sharpedge.keys():
if prev is None:
if this in sharpedge_clk.keys():
first = sharpedge[this]
else:
first = level.get(this, this)*3
else:
first = xclude.get(prev+this, sharpedge[this])
if this in sharpedge_clk.keys():
wave = [first, clkinvert[this]] * (1 + repeat)
else:
wave = [first] + [level.get(this, this)*3]*(2 * repeat + 1)
else:
if prev is None:
transition = level.get(this, this)*3 + data.get(this, "")
else:
transition = translevel.get(prev, prev) + 'm' + level.get(this, this) + data.get(prev, "") + data.get(this, "")
value = level.get(this, this)*3 + data.get(this, "")
wave = [transition, value] + [value, value] * repeat
if subcycle:
wave = wave[0:repeat+1]
if not (stretch == -0.5 and this in sharpedge_clk.keys()):
wave = self.stretch_bricks(wave, stretch)
return wave
def parse_wave_lane(self, text, stretch=0):
R = []
Stack = deque(text)
This = None
subCycle = False
while len(Stack) > 0:
Top = This
This = Stack.popleft()
repeat = 0
if This == '|':
This = 'x'
if This == '<':
subCycle = True
This = Top
Top = None
if Stack[0] in ['.', '|']:
Stack.popleft()
else:
continue
if This == '>':
subCycle = False
This = Top
Top = None
if Stack and Stack[0] in ['.', '|']:
Stack.popleft()
else:
continue
while Stack and Stack[0] in ['.', '|']:
Stack.popleft()
repeat += 1
R.extend(self.gen_wave_brick(Top, This, stretch, repeat, subCycle))
for i in range(int(math.ceil(self.lane.phase))):
R = R[1:]
return R
def parse_wave_lanes(self, sig=""):
def data_extract(e):
tmp = e.get("data")
if tmp is not None:
tmp = tmp.split() if isinstance(tmp, string_types) else tmp
return tmp
content = []
for sigx in sig:
self.lane.period = sigx.get("period", 1)
self.lane.phase = sigx.get("phase", 0) * 2
sub_content = []
sub_content.append([sigx.get("name", " "), sigx.get("phase", 0)])
if sigx.get("wave"):
sub_content.append(self.parse_wave_lane(sigx["wave"], self.lane.period * self.lane.hscale - 1))
else:
sub_content.append(None)
sub_content.append(data_extract(sigx))
content.append(sub_content)
return content
def find_lane_markers(self, lanetext=""):
lcount = 0
gcount = 0
ret = []
for idx, val in enumerate(lanetext):
if val in ["vvv-2", "vvv-3", "vvv-4", "vvv-5", "vvv-6", "vvv-7", "vvv-8", "vvv-9"]:
lcount += 1
else:
if lcount != 0:
ret.append(gcount - ((lcount + 1) / 2))
lcount = 0
gcount += 1
if lcount != 0:
ret.append(gcount - ((lcount + 1) / 2))
return ret
def render_lane_uses(self, val, g):
if val[1]:
for i in range(len(val[1])):
b = self.container.use(href="#{}".format(val[1][i]))
b.translate(i * self.lane.xs)
g.add(b)
if val[2] and len(val[2]):
labels = self.find_lane_markers(val[1])
if len(labels) != 0:
for k in range(len(labels)):
if val[2] and k < len(val[2]):
tx = int(labels[k]) * self.lane.xs + self.lane.xlabel
title = self.element.text("", x=[tx], y=[self.lane.ym], text_anchor="middle")
title.add(self.element.tspan(val[2][k]))
title["xml:space"] = "preserve"
g.add(title)
def text_width(self, string, size=11):
chars = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34,47,74,74,118,89,25,44,44,52,78,37,
44,37,37,74,74,74,74,74,74,74,74,74,74,37,37,78,78,78,74,135,89,89,96,96,89,81,103,96,37,67,89,74,109,
96,103,89,103,96,89,81,96,89,127,89,87,81,37,37,37,61,74,44,74,74,67,74,74,37,74,74,30,30,67,30,112,74,
74,74,74,44,67,37,74,67,95,66,65,67,44,34,44,78,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,37,43,74,74,74,74,34,74,44,98,49,74,78,0,98,73,53,73,44,44,44,77,71,37,44,44,49,74,111,111,
111,81,89,89,89,89,89,89,133,96,89,89,89,89,37,37,37,37,96,96,103,103,103,103,103,78,103,96,96,96,96,
87,89,81,74,74,74,74,74,74,118,67,74,74,74,74,36,36,36,36,74,74,74,74,74,74,74,73,81,74,74,74,74,65,74,
65,89,74,89,74,89,74,96,67,96,67,96,67,96,67,96,82,96,74,89,74,89,74,89,74,89,74,89,74,103,74,103,74,
103,74,103,74,96,74,96,74,37,36,37,36,37,36,37,30,37,36,98,59,67,30,89,67,67,74,30,74,30,74,39,74,44,
74,30,96,74,96,74,96,74,80,96,74,103,74,103,74,103,74,133,126,96,44,96,44,96,44,89,67,89,67,89,67,89,
67,81,38,81,50,81,37,96,74,96,74,96,74,96,74,96,74,96,74,127,95,87,65,87,81,67,81,67,81,67,30,84,97,91,
84,91,84,94,92,73,104,109,91,84,81,84,100,82,76,74,103,91,131,47,40,99,77,37,79,130,100,84,104,114,87,
126,101,87,84,93,84,69,84,46,52,82,52,82,114,89,102,96,100,98,91,70,88,88,77,70,85,89,77,67,84,39,65,
61,39,189,173,153,111,105,61,123,123,106,89,74,37,30,103,74,96,74,96,74,96,74,96,74,96,74,81,91,81,91,
81,130,131,102,84,103,84,87,78,104,81,104,81,88,76,37,189,173,153,103,84,148,90,100,84,89,74,133,118,
103,81]
return sum([(chars[ord(c)] if ord(c) <= len(chars) else 114) for c in string])*size/100
def render_wave_lane(self, content="", index=0):
xmax = 0
xgmax = 0
glengths = []
groups = []
for j, val in enumerate(content):
name = val[0][0].strip()
if name is not None:
dy = self.lane.y0 + j * self.lane.yo
g = self.container.g(id="wavelane_{j}_{index}".format(j=j, index=index))
g.translate(0, dy)
title = self.element.text("", x=[self.lane.tgo], y=[self.lane.ym], text_anchor="end")
title.add(self.element.tspan(name))
title["xml:space"] = "preserve"
title["class"] = "info"
g.add(title)
glengths.append(self.text_width(name))
xoffset = val[0][1]
xoffset = math.ceil(2 * xoffset) - 2 * xoffset if xoffset > 0 else -2 * xoffset
gg = self.container.g(id="wavelane_draw_{j}_{index}".format(j=j, index=index))
gg.translate(xoffset * self.lane.xs, 0)
self.render_lane_uses(val, gg)
if val[1] and len(val[1]) > xmax:
xmax = len(val[1])
g.add(gg)
groups.append(g)
self.lane.xmax = xmax
self.lane.xg = xgmax + 20
return (glengths, groups)
def captext(self, g, cxt, anchor, y):
if cxt.get(anchor) and cxt[anchor].get("text"):
tmark = self.element.text("", x=[float(cxt.xmax)*float(cxt.xs)/2], y=[y], text_anchor="middle", fill="#000")
tmark["xml:space"] = "preserve"
if isinstance(cxt[anchor]["text"], string_types):
tmark.add(self.element.tspan(cxt[anchor]["text"]))
else:
tmark.add(JsonMLElement(cxt[anchor]["text"]))
g.add(tmark)
def ticktock(self, g, cxt, ref1, ref2, x, dx, y, length):
L = []
if cxt.get(ref1) is None or cxt[ref1].get(ref2) is None:
return
val = cxt[ref1][ref2]
if isinstance(val, string_types):
val = val.split()
elif isinstance(val, (int, float, bool)):
offset = int(val)
val = []
for i in range(length):
val.append(i + offset)
if type(val) is list:
if len(val) == 0:
return
elif len(val) == 1:
offset = val[0]
if isinstance(offset, string_types):
L = val
else:
for i in range(length):
L[i] = i + offset
elif len(val) == 2:
offset = int(val[0])
step = int(val[1])
tmp = val[1].split(".")
if len(tmp) == 2:
dp = len(tmp[1])
if isinstance(offset, string_types) or isinstance(step, string_types):
L = val
else:
offset = step * offset
for i in range(length):
L[i] = "{0:.", dp, "f}".format(step * i + offset)
else:
L = val
else:
return
for i in range(length):
tmp = L[i]
tmark = self.element.text(tmp, x=[i * dx + x], y=[y], text_anchor="middle")
tmark["class"] = "muted"
tmark["xml:space"] = "preserve"
g.add(tmark)
def render_marks(self, content="", index=0):
def get_elem(e):
if len(e) == 3:
ret = self.element[e[0]](e[2])
ret.attribs = e[1]
elif len(e) == 2:
ret = self.element[e[0]](e[1])
else:
ret = self.element.tspan(e)
return ret
mstep = 2 * int(self.lane.hscale)
mmstep = mstep * self.lane.xs
marks = int(self.lane.xmax / mstep)
gy = len(content) * int(self.lane.yo)
g = self.container.g(id="gmarks_{}".format(index))
for i in range(marks + 1):
gg = self.element.path(id="gmark_{i}_{index}".format(i=i, index=index),
d="m {dx},0 0,{gy}".format(dx=i * mmstep, gy=gy),
style="stroke:#888;stroke-width:0.5;stroke-dasharray:1,3")
g.add(gg)
self.captext(g, self.lane, "head", -33 if (self.lane.yh0 > 0) else -13)
self.captext(g, self.lane, "foot", gy + (45 if (self.lane.yf0 > 0) else 25))
self.ticktock(g, self.lane, "head", "tick", 0, mmstep, -5, marks + 1)
self.ticktock(g, self.lane, "head", "tock", mmstep / 2, mmstep, -5, marks)
self.ticktock(g, self.lane, "foot", "tick", 0, mmstep, gy + 15, marks + 1)
self.ticktock(g, self.lane, "foot", "tock", mmstep / 2, mmstep, gy + 15, marks)
return g
def render_labels(self, root, source, index):
if source:
gg = self.container.g(id="labels_{index}".format(index=index))
for idx, val in enumerate(source):
self.lane.period = val.get("period", 1)
self.lane.phase = val.get("phase", 0) * 2
dy = self.lane.y0 + idx * self.lane.yo
g = self.container.g(id="labels_{i}_{index}".format(i=idx, index=index))
g.translate(0, dy)
label = val.get("label")
if label:
pos = 0
for l in re.findall(r"([\.\w]|(?:\{\w+\}))(?:\((\d*\.?\d+)\))?", label):
if l[0] == ".":
pos += 1
continue
text = l[0]
try:
offset = float(l[1])
except ValueError:
offset = 0
m = re.match(r"\{(\w+)\}", l[0])
if m:
text = m.group(1)
x = int(float(self.lane.xs) * (2 * (pos + offset) * self.lane.period *
self.lane.hscale - self.lane.phase) + float(self.lane.xlabel))
y = int(idx * self.lane.yo + self.lane.y0 + float(self.lane.ys) * 0.5) - dy
lwidth = len(text) * self.font_width
lx = float(x) - float(lwidth) / 2
ly = int(y) - 5
underlabel = self.element.rect(insert=(lx, ly),
size=(lwidth, 8), style="fill:#FFF;")
g.add(underlabel)
lx = float(x)
ly = int(y) + 2
label = self.element.text(text, style="font-size:8px;", text_anchor="middle",
x=[lx], y=[ly])
g.add(label)
pos += 1
gg.add(g)
root.add(gg)
def arc_shape(self, Edge, frm, to):
dx = float(to.x) - float(frm.x)
dy = float(to.y) - float(frm.y)
lx = (float(frm.x) + float(to.x)) / 2
ly = (float(frm.y) + float(to.y)) / 2
const_style = AttrDict({
"a": "marker-end:url(#arrowhead);stroke:#0041c4;stroke-width:1;fill:none",
"b": "marker-end:url(#arrowhead);marker-start:url(#arrowtail);stroke:#0041c4;stroke-width:1;fill:none"
})
pattern = {
"-": { },
"~": {"d": "M {fx},{fy} c {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(0.7 * dx), dy=0,
dxx=(0.3 * dx), dyy=dy,
dxxx=dx, dyyy=dy)},
"-~": {"d": "M {fx},{fy} c {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(0.7 * dx), dy=0,
dxx=dx, dyy=dy,
dxxx=dx, dyyy=dy)},
"~-": {"d": "M {fx},{fy} c {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=0, dy=0,
dxx=(0.3 * dx), dyy=dy,
dxxx=dx, dyyy=dy)},
"-|": {"d": "m {fx},{fy} {dx},{dy} {dxx},{dyy}".format(fx=frm.x, fy=frm.y,
dx=dx, dy=0,
dxx=0, dyy=dy)},
"|-": {"d": "m {fx},{fy} {dx},{dy} {dxx},{dyy}".format(fx=frm.x, fy=frm.y,
dx=0, dy=dy,
dxx=dx, dyy=0)},
"-|-": {"d": "m {fx},{fy} {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(dx / 2), dy=0,
dxx=0, dyy=dy,
dxxx=(dx / 2), dyyy=0)},
"->": {"style": const_style.a},
"~>": {"style": const_style.a,
"d": "M {fx},{fy} c {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(0.7 * dx), dy=0,
dxx=(0.3 * dx), dyy=dy,
dxxx=dx, dyyy=dy)},
"-~>": {"style": const_style.a,
"d": "M {fx},{fy} c {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(0.7 * dx), dy=0,
dxx=dx, dyy=dy,
dxxx=dx, dyyy=dy)},
"~->": {"style": const_style.a,
"d": "M {fx},{fy} c {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=0, dy=0,
dxx=(0.3 * dx), dyy=dy,
dxxx=dx, dyyy=dy)},
"-|>": {"style": const_style.a,
"d": "m {fx},{fy} {dx},{dy} {dxx},{dyy}".format(fx=frm.x, fy=frm.y,
dx=dx, dy=0,
dxx=0, dyy=dy)},
"|->": {"style": const_style.a,
"d": "m {fx},{fy} {dx},{dy} {dxx},{dyy}".format(fx=frm.x, fy=frm.y,
dx=0, dy=dy,
dxx=dx, dyy=0
)},
"-|->": {"style": const_style.a,
"d": "m {fx},{fy} {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(dx / 2), dy=0,
dxx=0, dyy=dy,
dxxx=(dx / 2), dyyy=0
)},
"<->": {"style": const_style.b},
"<~>": {"style": const_style.b,
"d": "M {fx},{fy} c {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(0.7 * dx), dy=0,
dxx=(0.3 * dx), dyy=dy,
dxxx=dx, dyyy=dy
)},
"<-~>": {"style": const_style.b,
"d": "M {fx},{fy} c {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(0.7 * dx), dy=0,
dxx=dx, dyy=dy,
dxxx=dx, dyyy=dy
)},
"<-|>": {"style": const_style.b,
"d": "m {fx},{fy} {dx},{dy} {dxx},{dyy}".format(fx=frm.x, fy=frm.y,
dx=dx, dy=0,
dxx=0, dyy=dy
)},
"<-|->": {"style": const_style.b,
"d": "m {fx},{fy} {dx},{dy} {dxx},{dyy} {dxxx},{dyyy}".format(fx=frm.x, fy=frm.y,
dx=(dx / 2), dy=0,
dxx=0, dyy=dy,
dxxx=(dx / 2), dyyy=0,
)}
}
props = AttrDict({"lx": lx, "ly": ly, "style": "fill:none;stroke:#00F;stroke-width:1",
"d": "M {fx},{fy} {tx},{ty}".format(fx=frm.x, fy=frm.y, tx=to.x, ty=to.y)})
if Edge.shape in pattern:
props.d = pattern[Edge.shape].get("d", props.d)
props.style = pattern[Edge.shape].get("style", props.style)
if Edge.label:
if Edge.shape in ["-~", "-~>", "<-~>"]:
props.lx = float(frm.x) + (float(to.x) - float(frm.x)) * 0.75
elif Edge.shape in ["~-", "~->"]:
props.lx = float(frm.x) + (float(to.x) - float(frm.x)) * 0.25
elif Edge.shape in ["-|", "-|>", "<-|>"]:
props.lx = float(to.x)
elif Edge.shape in ["|-", "|->"]:
props.lx = float(frm.x)
return props
def render_arc(self, Edge, frm, to, shapeProps):
return self.element.path(id="gmark_{frm}_{to}".format(frm=Edge.frm, to=Edge.to),
d=shapeProps.d, style=shapeProps.style)
def render_label(self, p, text):
w = self.text_width(text,8) + 2
g = self.container.g(transform = "translate({},{})".format(p.x, p.y))
# todo: I don't think this is correct. reported:
# https://github.com/wavedrom/wavedrom/issues/252
rect = self.element.rect(insert=(int(0-w/2), -5), size=(w, 10), style="fill:#FFF;")
label = self.element.text("", style="font-size:8px;", text_anchor="middle", y=[3])
label.add(self.element.tspan(text))
g.add(rect)
g.add(label)
return g
def render_arcs(self, source, index, top):
Edge = AttrDict({"words": [], "frm": 0, "shape": "", "to": 0, "label": ""})
Events = AttrDict({})
if source:
for idx, val in enumerate(source):
self.lane.period = val.get("period", 1)
self.lane.phase = val.get("phase", 0) * 2
text = val.get("node")
if text:
Stack = list(text)
Stack.reverse()
pos = 0
step = 1
while len(Stack) > 0:
eventname = Stack.pop()
if eventname == "<":
step = 0.25
continue
elif eventname == ">":
step = 1
continue
x = int(float(self.lane.xs) * (2 * pos * self.lane.period *
self.lane.hscale - self.lane.phase) + float(self.lane.xlabel))
y = int(idx * self.lane.yo + self.lane.y0 + float(self.lane.ys) * 0.5)
if eventname != ".":
Events[eventname] = AttrDict({"x": str(x), "y": str(y)})
pos += step
gg = self.container.g(id="wavearcs_{index}".format(index=index))
if top.get("edge"):
for i, val in enumerate(top["edge"]):
Edge.words = val.split()
Edge.label = val[len(Edge.words[0]):]
Edge.label = Edge.label[1:]
Edge.frm = Edge.words[0][0]
Edge.to = Edge.words[0][-1]
Edge.shape = Edge.words[0][1:-1]
frm = AttrDict(Events[Edge.frm])
to = AttrDict(Events[Edge.to])
shapeProps = self.arc_shape(Edge, frm, to)
gg.add(self.render_arc(Edge, frm, to, shapeProps))
if Edge.label:
gg.add(self.render_label(AttrDict({"x": shapeProps.lx, "y": shapeProps.ly}), Edge.label))
for k in Events:
if k.islower() or k.isdigit():
if int(Events[k].x) > 0:
gg.add(self.render_label(AttrDict({"x": Events[k].x, "y": Events[k].y}), k))
return gg
def parse_config(self, source={}):
self.lane.hscale = 1
if self.lane.get("hscale0"):
self.lane.hscale = self.lane.hscale0
if source and source.get("config") and source.get("config").get("hscale"):
hscale = round(source.get("config").get("hscale"))
if hscale > 0:
if hscale > 100:
hscale = 100
self.lane.hscale = hscale
self.lane.xmin_cfg = 0
self.lane.xmax_cfg = sys.maxsize
if source and "config" in source and "hbounds" in source["config"]:
if len(source["config"]["hbounds"]) == 2:
source["config"]["hbounds"][0] = math.floor(source["config"]["hbounds"][0])
source["config"]["hbounds"][1] = math.ceil(source["config"]["hbounds"][0])
if source["config"]["hbounds"][0] < source["config"]["hbounds"][1]:
self.lane.xmin_cfg = 2 * source["config"]["hbounds"][0]
self.lane.xmax_cfg = 2 * source["config"]["hbounds"][1]
self.lane.yh0 = 0
self.lane.yh1 = 0
if source and source.get("head"):
self.lane.head = source["head"]
if "tick" in source["head"] or "tock" in source["head"]:
self.lane.yh0 = 20
if "tick" in source["head"]:
source["head"]["tick"] += self.lane.xmin_cfg/2
if "tock" in source["head"]:
source["head"]["tock"] += self.lane.xmin_cfg/2
if source.get("head").get("text"):
self.lane.yh1 = 46
self.lane.head["text"] = source["head"]["text"]
self.lane.yf0 = 0
self.lane.yf1 = 0
if source and source.get("foot"):
self.lane.foot = source["foot"]
if "tick" in source["foot"] or "tock" in source["foot"]:
self.lane.yf0 = 20
if "tick" in source["foot"]:
source["foot"]["tick"] += self.lane.xmin_cfg/2
if "tock" in source["foot"]:
source["foot"]["tock"] += self.lane.xmin_cfg/2
if source.get("foot").get("text"):
self.lane.yf1 = 46
self.lane.foot["text"] = source["foot"]["text"]
def rec(self, tmp=[], state={}):
name = None
delta = AttrDict({"x": 10})
if isinstance(tmp[0], str) or isinstance(tmp[0], int):
name = str(tmp[0])
delta.x = 25
state.x += delta.x
for idx, val in enumerate(tmp):
if isinstance(val, list):
old_y = state.y
self.rec(val, state)
state["groups"].append({"x": state.xx,
"y": old_y,
"height": state.y - old_y,
"name": state.name})
elif isinstance(val, dict):
state["lanes"].append(val)
state["width"].append(state.x)
state.y += 1
state.xx = state.x
state.x -= delta.x
state.name = name
def another_template(self, index, source):
def get_container(elem):
ctype = elem[0]
ret = self.container[ctype]()
ret.attribs = elem[1]
def gen_elem(e):
if e[0] == "path":
attr = e[1]
elem = self.element.path(d=attr["d"])
elem.attribs = attr
elif e[0] == "rect":
attr = e[1]
x = attr["x"]
y = attr["y"]
w = attr["width"]
h = attr["height"]
elem = self.element.rect(insert=(x, y), size=(w, h))
elem.attribs = attr
return elem
[ret.add(gen_elem(e)) for e in elem[2:]]
return ret
skinname = source.get("config", {"skin" : "default"}).get("skin", "default")
skin = waveskin.WaveSkin.get(skinname, waveskin.WaveSkin["default"])
template = svgwrite.Drawing(id="svgcontent_{index}".format(index=index))
if index == 0:
template.add(template.style(skin[2][2]))
[template.defs.add(get_container(e)) for e in skin[3][1:]]
self.lane.xs = int(skin[3][1][2][1]["width"])
self.lane.ys = int(skin[3][1][2][1]["height"])
self.lane.xlabel = int(skin[3][1][2][1]["x"])
self.lane.ym = int(skin[3][1][2][1]["y"])
template["class"] = "WaveDrom"
template["overflow"] = "hidden"
return template
def insert_svg_template(self, index=0, parent=[], source={}):
e = waveskin.WaveSkin["default"]
if source.get("config") and source.get("config").get("skin"):
if waveskin.WaveSkin.get(source.get("config").get("skin")):
e = waveskin.WaveSkin[source.get("config").get("skin")]
if index == 0:
self.lane.xs = int(e[3][1][2][1]["width"])
self.lane.ys = int(e[3][1][2][1]["height"])
self.lane.xlabel = int(e[3][1][2][1]["x"])
self.lane.ym = int(e[3][1][2][1]["y"])
else:
e = ["svg",
{"id": "svg",
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
"height": "0"},
[ # e[-1]
"g", # e[-1][0]
{"id": "waves"}, # e[-1][1]
[ # e[-1][2]
"g", # e[-1][2][0]
{"id": "lanes"} # e[-1][2][1]
],
[ # e[-1][3]
"g", # e[-1][3][0]
{"id": "groups"} # e[-1][3][1]
]
]
]
e[-1][1]["id"] = "waves_{index}".format(index=index)
e[-1][2][1]["id"] = "lanes_{index}".format(index=index)
e[-1][3][1]["id"] = "groups_{index}".format(index=index)
e[1]["id"] = "svgcontent_{index}".format(index=index)
e[1]["height"] = 0
parent.extend(e)
def render_waveform(self, index=0, source={}, output=[], strict_js_features=False):
xmax = 0
if source.get("signal"):
template = self.another_template(index, source)
waves = template.g(id="waves_{index}".format(index=index))
lanes = template.g(id="lanes_{index}".format(index=index))
groups = template.g(id="groups_{index}".format(index=index))
self.parse_config(source)
ret = AttrDict({"x": 0, "y": 0, "xmax": 0, "width": [], "lanes": [], "groups": []})
self.rec(source["signal"], ret) # parse lanes
content = self.parse_wave_lanes(ret.lanes)
(glengths, lanegroups) = self.render_wave_lane(content, index)
for i, val in enumerate(glengths):
xmax = max(xmax, (val + ret.width[i]))
marks = self.render_marks(content, index)
gaps = self.render_gaps(ret.lanes, index)
if not strict_js_features:
self.render_labels(lanes, ret.lanes, index)
arcs = self.render_arcs(ret.lanes, index, source)
# Render
lanes.add(marks)
[lanes.add(l) for l in lanegroups]
lanes.add(arcs)
lanes.add(gaps)
self.render_groups(groups, ret.groups, index)
self.lane.xg = int(math.ceil(float(xmax - self.lane.tgo) / float(self.lane.xs))) * self.lane.xs
width = self.lane.xg + self.lane.xs * (self.lane.xmax + 1)
height = len(content) * self.lane.yo + self.lane.yh0 + self.lane.yh1 + self.lane.yf0 + self.lane.yf1
template["width"] = width
template["height"] = height
template.viewbox(0, 0, width, height)
dx = self.lane.xg + 0.5
dy = float(self.lane.yh0) + float(self.lane.yh1) + 0.5
lanes.translate(dx, dy)
waves.add(lanes)
waves.add(groups)
template.add(waves)
return template
def render_groups(self, root=[], groups=[], index=0):
for i, val in enumerate(groups):
dx = groups[i]["x"] + 0.5
dy = groups[i]["y"] * self.lane.yo + 3.5 + self.lane.yh0 + self.lane.yh1
h = int(groups[i]["height"] * self.lane.yo - 16)
group = self.element.path(id="group_{i}_{index}".format(i=i, index=index),
d="m {dx},{dy} c -3,0 -5,2 -5,5 l 0,{h} c 0,3 2,5 5,5".format(dx=dx, dy=dy, h=h),
style="stroke:#0041c4;stroke-width:1;fill:none")
root.add(group)
name = groups[i]["name"]
x = int(groups[i]["x"] - 10)
y = int(self.lane.yo * (groups[i]["y"] + (float(groups[i]["height"]) / 2)) +
self.lane.yh0 + self.lane.yh1)
label = self.container.g()
label.translate(x, y)
gg = self.container.g()
gg.rotate(270)
t = self.element.text("", text_anchor="middle")
t["class"] = "info"
t["xml:space"] = "preserve"
t.add(self.element.tspan(name))
gg.add(t)
label.add(gg)
root.add(label)
def render_gap_uses(self, wave, g):
subCycle = False
if wave:
Stack = deque(wave)
pos = 0
while len(Stack):
next = Stack.popleft()
if next == '<':
subCycle = True
continue
if next == '>':
subCycle = False
continue
if subCycle:
pos += self.lane.period
else:
pos += 2 * self.lane.period
if next == "|":
if subCycle:
dx = float(self.lane.xs) * (pos * float(self.lane.hscale) - float(self.lane.phase))
else:
dx = float(self.lane.xs) * ((pos - self.lane.period) * float(self.lane.hscale) - float(self.lane.phase))
b = self.container.use(href="#gap")
b.translate(dx)
g.add(b)
def render_gaps(self, source, index):
if source:
gg = self.container.g(id="wavegaps_{index}".format(index=index))
for idx, val in enumerate(source):
self.lane.period = val.get("period", 1)
self.lane.phase = int(val.get("phase", 0) * 2) + self.lane.xmin_cfg
dy = self.lane.y0 + idx * self.lane.yo
g = self.container.g(id="wavegap_{i}_{index}".format(i=idx, index=index))
g.translate(0, dy)
if "wave" in val:
self.render_gap_uses(val["wave"], g)
gg.add(g)
return gg
def convert_to_svg(self, root):
svg_output = ""
if type(root) is list:
if len(root) >= 2 and type(root[1]) is dict:
if len(root) == 2:
svg_output += "<{}{}/>\n".format(root[0], self.convert_to_svg(root[1]))
elif len(root) >= 3:
svg_output += "<{}{}/>\n".format(root[0], self.convert_to_svg(root[1]))
if len(root) == 3:
svg_output += self.convert_to_svg(root[2])
else:
svg_output += self.convert_to_svg(root[2:])
svg_output += "</{}>\n".format(root[0])
elif type(root[0]) is list:
for eleml in root:
svg_output += self.convert_to_svg(eleml)
else:
svg_output += "<{}>\n".format(root[0])
for eleml in root[1:]:
svg_output += self.convert_to_svg(eleml)
svg_output += "</{}>\n".format(root[0])
elif type(root) is dict:
for elemd in root:
svg_output += " {}=\"{}\"".format(elemd, root[elemd])
else:
svg_output += root
return svg_output
# Backward compatibility
genWaveBrick = gen_wave_brick
parseWaveLane = parse_wave_lane
parseWaveLanes = parse_wave_lanes
findLaneMarkers = find_lane_markers
renderWaveLane = render_wave_lane
renderMarks = render_marks
renderLabels = render_labels
renderArcs = render_arcs
parseConfig = parse_config
anotherTemplate = another_template
insertSVGTemplate = insert_svg_template
renderWaveForm = render_waveform
renderGroups = render_groups
renderGaps = render_gaps
File diff suppressed because it is too large Load Diff