mirror of https://gerrit.osmocom.org/libosmocore
add contrib/ladder_to_msc.py
Typing mscgen diagrams, I am hugely annoyed by having to type '[label="..."]' all the time. Also, IMHO the arrows have been chosen in an unintuitive way: in mscgen, '=>>' is the normal arrow, and '->' is a half-headed arrow, etc. I would like to use other arrow symbols. Hence, add script to convert my personal favorite ascii format for message sequence charts to mscgen format. See an example in ladder_to_msc_test.ladder. Change-Id: Iefac4cb91b82c93a64b4999afa62e299479913af
This commit is contained in:
parent
a41bd22349
commit
a6126f407b
|
@ -0,0 +1,10 @@
|
|||
png: \
|
||||
ladder_to_msc_test.png \
|
||||
$(NULL)
|
||||
|
||||
%.png: %.msc
|
||||
mscgen -T png -o $@ $<
|
||||
|
||||
%.msc: %.ladder
|
||||
@which ladder_to_msc.py || (echo 'PLEASE POINT YOUR $$PATH AT libosmocore/contrib/ladder_to_msc.py' && false)
|
||||
ladder_to_msc.py -i $< -o $@
|
|
@ -0,0 +1,401 @@
|
|||
#!/usr/bin/env python3
|
||||
doc=r'''
|
||||
We write a lot of ladder diagrams to explain CNI procedures.
|
||||
However, the .msc format has a lot of overhead for a human author:
|
||||
|
||||
foo[label="Foo"], bar[label="Bar"];
|
||||
foo -> bar [label="Baz msg"];
|
||||
foo <= bar [label="Moo",attrib="value",attrib2="value2"];
|
||||
foo -> bar [label="Multi\nLine\nDescription"];
|
||||
|
||||
This defines a .ladder format that is easier to type and can be directly translated into .msc.
|
||||
|
||||
foo = Foo
|
||||
bar = Bar
|
||||
|
||||
foo > bar Baz msg
|
||||
foo << bar Moo
|
||||
{attrib=value, attrib2=value2}
|
||||
|
||||
foo > bar
|
||||
Multi
|
||||
Line
|
||||
Description
|
||||
|
||||
|
||||
a > b simple arrow
|
||||
a -> b filled arrow
|
||||
a => b double-lined arrow
|
||||
a --> b dashed-line arrow
|
||||
a ~> b half arrow-head
|
||||
a ->< b arrow with X arrowhead
|
||||
a > * broadcast arrow with multiple heads
|
||||
|
||||
'''
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import re
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
def error(*msg):
|
||||
sys.stderr.write('%s\n' % (''.join(msg)))
|
||||
exit(1)
|
||||
|
||||
def quote(msg, quote='"'):
|
||||
return '"%s"' % (msg.replace('"', r'\"'))
|
||||
|
||||
class Entity:
|
||||
def __init__(self):
|
||||
self.name = None
|
||||
self.descr = None
|
||||
self.attrs = {}
|
||||
|
||||
class Arrow:
|
||||
def __init__(self):
|
||||
self.left = None
|
||||
self.arrow = None
|
||||
self.right = None
|
||||
self.descr = None
|
||||
self.attrs = {}
|
||||
|
||||
class Output:
|
||||
def __init__(self, write_to):
|
||||
self._write_to = write_to
|
||||
self.collected_entities = []
|
||||
self.empty_lines_after_entities = 0
|
||||
|
||||
def write(self, line):
|
||||
self._write_to.write(line)
|
||||
|
||||
def txlate_entity_name(self, name):
|
||||
if name == 'msc':
|
||||
return '__msc'
|
||||
return name
|
||||
|
||||
def start(self):
|
||||
self.write('msc {\n');
|
||||
def end(self):
|
||||
self.write('}\n');
|
||||
|
||||
def writeln(self, line):
|
||||
self.write(' %s;\n' % line)
|
||||
|
||||
def root_attrs(self, attrs):
|
||||
self.writeln(','.join('%s=%s' % (k,quote(v)) for k,v in attrs.items()))
|
||||
|
||||
def entity(self, e):
|
||||
self.collected_entities.append(e)
|
||||
|
||||
def entities(self):
|
||||
line = []
|
||||
for e in self.collected_entities:
|
||||
attr_strs = []
|
||||
if e.descr:
|
||||
attr_strs.append('label=%s' % quote(e.descr))
|
||||
for k,v in e.attrs.items():
|
||||
attr_strs.append('%s=%s' % (k, quote(v)))
|
||||
|
||||
if attr_strs:
|
||||
line.append('%s[%s]' % (self.txlate_entity_name(e.name), ','.join(attr_strs)))
|
||||
else:
|
||||
line.append(self.txlate_entity_name(e.name))
|
||||
self.writeln('%s' % (','.join(line)))
|
||||
self.collected_entities = []
|
||||
if self.empty_lines_after_entities:
|
||||
self.write('\n' * self.empty_lines_after_entities);
|
||||
self.empty_lines_after_entities = 0
|
||||
|
||||
def left_arrow_right(self, arrow):
|
||||
if self.collected_entities:
|
||||
self.entities()
|
||||
|
||||
line = [self.txlate_entity_name(arrow.left), arrow.arrow, self.txlate_entity_name(arrow.right)]
|
||||
attrs = []
|
||||
if arrow.descr:
|
||||
attrs.append('label=%s' % quote(arrow.descr))
|
||||
for k,v in arrow.attrs.items():
|
||||
attrs.append('%s=%s' % (k, quote(v)))
|
||||
if attrs:
|
||||
line.append('[%s]' % (','.join(attrs)))
|
||||
self.writeln(' '.join(line))
|
||||
|
||||
def separator(self, sep_str, descr, attrs):
|
||||
if self.collected_entities:
|
||||
self.entities()
|
||||
|
||||
a = []
|
||||
if descr.strip():
|
||||
a.append('label=%s' % quote(descr))
|
||||
for k,v in attrs.items():
|
||||
a.append('%s=%s' % (k, quote(v)))
|
||||
if not a:
|
||||
self.writeln(sep_str)
|
||||
else:
|
||||
self.writeln('%s [%s]' % (sep_str, ','.join(a)))
|
||||
|
||||
def empty_line(self, count):
|
||||
if self.collected_entities:
|
||||
self.empty_lines_after_entities += count
|
||||
return
|
||||
self.write('\n' * count);
|
||||
|
||||
class Parse:
|
||||
RE_ENTITY = re.compile(r'^([a-zA-Z0-9_]+)[ \t]*(|=[ \t]*([a-zA-Z].+))$')
|
||||
RE_LEFT_ARROW_RIGHT = re.compile(r'^([^ \t<=>()[\]-]+)([ \t]*([<=>[\]():\\/|~-]+)[ \t]*|[ \t]+([a-z]+)[ \t]+)([a-zA-Z0-9_-]+|\*|\.)([ \t]*|[ \t]+(.*)|\\n(.*))$')
|
||||
RE_SEPARATOR = re.compile(r'^(\.\.\.|\|\|\||---)[ \t]*(.*)$')
|
||||
RE_INDENT = re.compile(r'^([ \t]+).*')
|
||||
RE_ATTR = re.compile(r'[{,]([a-zA-Z0-9_-]+)[ \t]*=[ \t]*([^,}]+)')
|
||||
RE_ATTRS_STR = re.compile(r'(.*?)[ \t]*({[^}]+=[^}]+})[ \t]*$')
|
||||
ARROWS = {
|
||||
'>' : '=>>',
|
||||
'->' : '=>',
|
||||
'-->' : '>>',
|
||||
'~>' : '->',
|
||||
'=>' : ':>',
|
||||
'-><' : '-x',
|
||||
|
||||
'<' : '<<=',
|
||||
'<-' : '<=',
|
||||
'<--' : '<<',
|
||||
'<~' : '<-',
|
||||
'<=' : '<:',
|
||||
'><-' : 'x-',
|
||||
|
||||
'<>' : 'abox',
|
||||
'()' : 'rbox',
|
||||
'[]' : 'note',
|
||||
|
||||
'<->' : '<=>',
|
||||
'<-->' : '<<=>>',
|
||||
'<~>' : '<->',
|
||||
'<=>' : '<:>',
|
||||
}
|
||||
|
||||
def __init__(self, output):
|
||||
self.line_block = []
|
||||
self.line_block_started_at = 1
|
||||
self.output = output
|
||||
self.linenr = 0
|
||||
|
||||
def error(self, *msg):
|
||||
error('line %d: ' % self.line_block_started_at, *msg)
|
||||
|
||||
def start(self):
|
||||
self.output.start()
|
||||
def end(self):
|
||||
self.output.end()
|
||||
|
||||
def add_line(self, line):
|
||||
self.linenr += 1
|
||||
if line.endswith('\n'):
|
||||
line = line[:-1]
|
||||
if line.endswith('\r'):
|
||||
line = line[:-1]
|
||||
|
||||
if line.strip().startswith('#'):
|
||||
self.output.writeln(line)
|
||||
return
|
||||
|
||||
if len(line) > 0 and not Parse.RE_INDENT.match(line):
|
||||
self.flush_block()
|
||||
self.line_block.append(line)
|
||||
|
||||
def flush_block(self):
|
||||
block = self.line_block
|
||||
self.line_block = []
|
||||
|
||||
# strip trailing empty lines
|
||||
empties = 0
|
||||
while len(block) and not block[-1].strip():
|
||||
block = block[:-1]
|
||||
empties += 1
|
||||
|
||||
self.interpret(block)
|
||||
if empties:
|
||||
self.output.empty_line(empties)
|
||||
|
||||
self.line_block_started_at = self.linenr
|
||||
|
||||
def interpret(self, block):
|
||||
# ignore empty blocks
|
||||
if not block:
|
||||
return
|
||||
|
||||
if block[0].startswith('{'):
|
||||
self.root_attrs(block)
|
||||
return
|
||||
|
||||
m = Parse.RE_ENTITY.match(block[0])
|
||||
if m:
|
||||
self.entity(block)
|
||||
return
|
||||
|
||||
m = Parse.RE_SEPARATOR.match(block[0])
|
||||
if m:
|
||||
self.separator(block)
|
||||
return
|
||||
|
||||
self.left_arrow_right(block)
|
||||
|
||||
def remove_indent(self, block):
|
||||
if len(block) == 1:
|
||||
return block
|
||||
first_nonempty_line = None
|
||||
for l in block[1:]:
|
||||
if not l:
|
||||
continue
|
||||
first_nonempty_line = l
|
||||
break
|
||||
if first_nonempty_line is None:
|
||||
return block
|
||||
m = Parse.RE_INDENT.match(first_nonempty_line)
|
||||
indent = m.group(1)
|
||||
content = [block[0]]
|
||||
for line in block[1:]:
|
||||
if not line.strip():
|
||||
content.append('')
|
||||
continue
|
||||
if not line.startswith(indent):
|
||||
self.error('Inconsistent indenting: expected %r, got %r' % (indent, line))
|
||||
content.append(line[len(indent):])
|
||||
return content
|
||||
|
||||
def root_attrs(self, block):
|
||||
block = self.remove_indent(block)
|
||||
attrs_str = ','.join(block)
|
||||
attrs = {}
|
||||
for m in Parse.RE_ATTR.finditer(attrs_str):
|
||||
key = m.group(1)
|
||||
val = m.group(2)
|
||||
attrs[key] = val
|
||||
self.output.root_attrs(attrs)
|
||||
|
||||
def entity(self, block):
|
||||
line = '\\n'.join(self.remove_indent(block))
|
||||
m = Parse.RE_ENTITY.match(line)
|
||||
if not m:
|
||||
self.error('Failure to parse entity like "foo = Description", got %r' % block[0])
|
||||
e = Entity()
|
||||
e.name = m.group(1)
|
||||
if len(m.groups()) > 2:
|
||||
e.descr = m.group(3)
|
||||
self.output.entity(e)
|
||||
|
||||
def separator(self, block):
|
||||
attrs_str, block = self.remove_attrs_str(block)
|
||||
line = '\\n'.join(self.remove_indent(block))
|
||||
m = Parse.RE_SEPARATOR.match(line)
|
||||
if not m:
|
||||
self.error('Failure to parse separator like "... Description", got %r' % block[0])
|
||||
sep_str = m.group(1)
|
||||
descr = m.group(2)
|
||||
attrs = {}
|
||||
for m in Parse.RE_ATTR.finditer(attrs_str):
|
||||
key = m.group(1)
|
||||
val = m.group(2)
|
||||
attrs[key] = val
|
||||
self.output.separator(sep_str, descr, attrs)
|
||||
|
||||
def translate_arrow(self, arrow_str):
|
||||
if arrow_str in Parse.ARROWS:
|
||||
return Parse.ARROWS.get(arrow_str)
|
||||
if arrow_str in Parse.ARROWS.values():
|
||||
return arrow_str
|
||||
self.error('Unknown arrow string: %r' % arrow_str)
|
||||
|
||||
def remove_attrs_str(self, block):
|
||||
last_line = block[-1]
|
||||
m = Parse.RE_ATTRS_STR.match(last_line)
|
||||
if not m:
|
||||
return '', block
|
||||
|
||||
before = m.group(1)
|
||||
attrs_str = m.group(2)
|
||||
|
||||
if before:
|
||||
block[-1] = before
|
||||
else:
|
||||
block = block[:-1]
|
||||
return attrs_str, block
|
||||
|
||||
def left_arrow_right(self, block):
|
||||
attrs_str, block = self.remove_attrs_str(block)
|
||||
line = '\\n'.join(self.remove_indent(block))
|
||||
|
||||
m = Parse.RE_LEFT_ARROW_RIGHT.match(line)
|
||||
if not m:
|
||||
self.error('Expected a line like "foo > bar Comment", but got:\n%r' % block[0])
|
||||
a = Arrow()
|
||||
a.left = m.group(1)
|
||||
a.arrow = self.translate_arrow(m.group(3) or m.group(4))
|
||||
a.right = m.group(5)
|
||||
if a.right == '.':
|
||||
a.right = a.left
|
||||
a.descr = m.group(7) or m.group(8)
|
||||
|
||||
attrs = {}
|
||||
for m in Parse.RE_ATTR.finditer(attrs_str):
|
||||
key = m.group(1)
|
||||
val = m.group(2)
|
||||
attrs[key] = val
|
||||
a.attrs = attrs
|
||||
|
||||
if a.descr and a.descr.count('^') == 1 and not 'id' in [k.lower() for k in a.attrs.keys()]:
|
||||
normal, superscript = a.descr.split('^')
|
||||
if normal.strip():
|
||||
a.descr = normal
|
||||
a.attrs['ID'] = superscript
|
||||
|
||||
self.output.left_arrow_right(a)
|
||||
|
||||
|
||||
|
||||
def translate(inf, outf, cmdline):
|
||||
output = Output(outf)
|
||||
parse = Parse(output)
|
||||
|
||||
parse.start()
|
||||
|
||||
while inf.readable():
|
||||
line = inf.readline()
|
||||
if not line:
|
||||
break;
|
||||
parse.add_line(line)
|
||||
parse.flush_block()
|
||||
parse.end()
|
||||
|
||||
def open_output(inf, cmdline):
|
||||
if cmdline.output_file == '-':
|
||||
translate(inf, sys.stdout, cmdline)
|
||||
else:
|
||||
with tempfile.NamedTemporaryFile(dir=os.path.dirname(cmdline.output_file), mode='w', encoding='utf-8') as tmp_out:
|
||||
translate(inf, tmp_out, cmdline)
|
||||
if os.path.exists(cmdline.output_file):
|
||||
os.unlink(cmdline.output_file)
|
||||
os.link(tmp_out.name, cmdline.output_file)
|
||||
|
||||
def open_input(cmdline):
|
||||
if cmdline.input_file == '-':
|
||||
open_output(sys.stdin, cmdline)
|
||||
else:
|
||||
with open(cmdline.input_file, 'r') as f:
|
||||
open_output(f, cmdline)
|
||||
|
||||
def main(cmdline):
|
||||
open_input(cmdline)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=doc)
|
||||
parser.add_argument('-i', '--input-file', dest='input_file', default="-",
|
||||
help='Read from this file, or stdin if "-"')
|
||||
parser.add_argument('-o', '--output-file', dest='output_file', default="-",
|
||||
help='Write to this file, or stdout if "-"')
|
||||
|
||||
cmdline = parser.parse_args()
|
||||
|
||||
main(cmdline)
|
||||
|
||||
# vim: shiftwidth=8 noexpandtab tabstop=8 autoindent
|
|
@ -0,0 +1,63 @@
|
|||
{hscale=2}
|
||||
msc1 = osmo-msc
|
||||
foo = a Foo instance
|
||||
bar = a Barcode
|
||||
|
||||
|
||||
msc1 > foo Some description
|
||||
msc1 -> foo Some description
|
||||
msc1->foo Some description
|
||||
msc1 >> foo Some description
|
||||
multi
|
||||
line
|
||||
msc1 >> foo
|
||||
Some description\nwith line feed
|
||||
multi
|
||||
line
|
||||
msc1 <> foo Some description
|
||||
msc1 () foo Some description
|
||||
msc1()foo Some description
|
||||
msc1 [] foo Some description
|
||||
msc1 note foo Some description
|
||||
msc1 note foo Some description
|
||||
|
||||
|
||||
... asdf asdf {ID=*}
|
||||
|
||||
msc1 > foo Some description {id=bar}
|
||||
msc1 >> foo Some description {id=bar}
|
||||
|
||||
msc1 [] foo Red box {textbgcolor=red}
|
||||
||| yo
|
||||
|
||||
msc1 >> foo Some description {id=bar}
|
||||
|
||||
bar>msc1 Some description
|
||||
---
|
||||
|
||||
foo > bar normal arrow
|
||||
foo -> bar filled arrow
|
||||
foo --> bar stippled arrow
|
||||
foo ~> bar half arrowhead
|
||||
foo => bar double lined arrow
|
||||
foo ->< bar arrow that ends in X
|
||||
msc1 > * broadcast arrow
|
||||
foo --> * broadcast stippled arrow
|
||||
|
||||
foo < bar
|
||||
foo <- bar
|
||||
foo <-- bar
|
||||
foo <~ bar
|
||||
foo <= bar
|
||||
foo ><- bar
|
||||
* < bar
|
||||
|
||||
foo <-> bar
|
||||
foo <--> bar
|
||||
foo <~> bar
|
||||
foo <=> bar
|
||||
|
||||
foo <> . angled box
|
||||
foo () . rounded box
|
||||
foo [] . note
|
||||
|
Loading…
Reference in New Issue