aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--contrib/ladder_to_msc.makefile10
-rwxr-xr-xcontrib/ladder_to_msc.py401
-rw-r--r--contrib/ladder_to_msc_test.ladder63
3 files changed, 474 insertions, 0 deletions
diff --git a/contrib/ladder_to_msc.makefile b/contrib/ladder_to_msc.makefile
new file mode 100644
index 00000000..bd05b1dc
--- /dev/null
+++ b/contrib/ladder_to_msc.makefile
@@ -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 $@
diff --git a/contrib/ladder_to_msc.py b/contrib/ladder_to_msc.py
new file mode 100755
index 00000000..9bac110e
--- /dev/null
+++ b/contrib/ladder_to_msc.py
@@ -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
diff --git a/contrib/ladder_to_msc_test.ladder b/contrib/ladder_to_msc_test.ladder
new file mode 100644
index 00000000..319d6051
--- /dev/null
+++ b/contrib/ladder_to_msc_test.ladder
@@ -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
+