diff --git a/diagrams.py b/diagrams.py
new file mode 100644
index 0000000..3afcc93
--- /dev/null
+++ b/diagrams.py
@@ -0,0 +1,315 @@
+from subprocess import Popen, PIPE
+import sys
+from sys import platform
+from os.path import expanduser
+from pathlib import Path
+import os.path
+import zlib
+import re
+import json
+
+
+class Example(object):
+ """ Example handling """
+
+ @staticmethod
+ def get(generator):
+ scriptdir = os.path.dirname(os.path.abspath(__file__))
+ example = scriptdir + "/../web/examples/example.%s" % generator
+ if os.path.isfile(example):
+ txt = Path(example).read_text()
+ else:
+ txt = "no example for %s found" % generator
+ return txt
+
+
+class Command(object):
+ """ a command to be run using the shell environment """
+
+ def __init__(self, cmd, versionOption="--version", timeout=5, debug=False):
+ """ construct me """
+ self.cmd = cmd
+ self.timeout = timeout
+ self.versionOption = versionOption
+ self.cmdpath = None
+ self.debug = debug
+
+ def call(self, args):
+ """ call me with the given args"""
+ return self.docall(self.cmdpath, self.cmd, args)
+
+ def callalias(self, alias, args):
+ """ call me with the given args"""
+ #print ("callalias cmdpath %s\nalias %s\nargs %s" % (self.cmdpath, alias, args), file=sys.stderr)
+ return self.docall(self.cmdpath, alias, args)
+
+ def docall(self, cmdpath, cmd, args):
+ """ call with a specific path and command"""
+ cmdline = "%s%s %s" % (cmdpath, cmd, str(args))
+ if self.debug:
+ print ("calling %s" % cmdline, file=sys.stderr)
+ process = Popen(cmdline, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+ try:
+ stdout, stderr = process.communicate(timeout=self.timeout)
+ if self.debug:
+ if stdout is not None:
+ print ("stdout: %s" % stdout.decode('utf-8'), file=sys.stderr)
+ if stderr is not None:
+ print ("stderr: %s" % stderr.decode('utf-8'), file=sys.stderr)
+ return stdout, stderr
+ except Exception:
+ process.kill()
+ return None, None
+
+ def check(self):
+ cmdpaths = []
+ # do we know the cmdpath?
+ if self.cmdpath is None:
+ # no we need to try multiple options in a specific order
+ # prio #1: $HOME/bin
+ home = expanduser("~")
+ cmdpaths.append(home + "/bin/")
+ # prio #2: e.g. Macports
+ if platform == "darwin":
+ cmdpaths.append("/opt/local/bin/")
+ # prio #3: default path / no path
+ # no path - use default PATH
+ cmdpaths.append("")
+ else:
+ # we know the valid path
+ cmdpaths.append(self.cmdpath)
+ for cmdpath in cmdpaths:
+ stdout, stderr = self.docall(cmdpath, self.cmd, self.versionOption)
+ stdoutTxt = None
+ stderrTxt = None
+ if stdout is not None:
+ stdoutTxt = stdout.decode("utf-8")
+ if stderr is not None:
+ stderrTxt = stderr.decode("utf-8")
+ if not "not found" in stderrTxt and not "No such file or directory" in stderrTxt:
+ self.cmdpath = cmdpath
+ return stdoutTxt, stderrTxt
+ return None, None
+
+
+class Generators(object):
+ generatorDict = {}
+ """ the available generators """
+
+ @staticmethod
+ def get(generator):
+ if len(Generators.generatorDict) is 0:
+ for gen in Generators.generators():
+ Generators.generatorDict[gen.id] = gen
+ gen = None
+ if generator in Generators.generatorDict:
+ gen = Generators.generatorDict[generator]
+ return gen
+
+ @staticmethod
+ def generatorIdForAlias(alias):
+ for gen in Generators.generators():
+ if alias in gen.aliases:
+ return gen.id
+ return None
+
+ @staticmethod
+ def generators():
+ scriptdir = os.path.dirname(os.path.abspath(__file__))
+ for plantumlpath in [".", ".."]:
+ plantumljar = scriptdir + "/" + plantumlpath + "/plantuml.jar";
+ if os.path.isfile(plantumljar):
+ break;
+ if plantumljar is None:
+ raise Exception("plantuml.jar not found in %s or .. of it", scriptdir)
+ #else
+ gens = [
+ Generator("graphviz", "GraphViz", "dot", "-V", logo="https://graphviz.gitlab.io/_pages/Resources/app.png", url="https://www.graphviz.org/",
+ aliases=[ 'dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage' ],
+ defaultType='png',
+ outputTypes=['dot', 'xdot', 'ps', 'pdf', 'svg', 'fig', 'png', 'gif', 'jpg', 'json', 'imap', 'cmapx']
+ ),
+ Generator("mscgen", "Mscgen", "mscgen", "", logo="http://www.mcternan.me.uk/mscgen/img/msc-sig.png", url="http://www.mcternan.me.uk/mscgen/", defaultType='png', outputTypes=['png', 'eps', 'svg', 'ismap']),
+ Generator("plantuml", "PlantUML", "java -jar " + plantumljar, "-version", aliases=['plantuml'],
+ logo="https://useblocks.com/assets/img/posts/plantuml_logo.png",
+ url="https://plantuml.com",
+ defaultType='png',
+ download="http://sourceforge.net/projects/plantuml/files/plantuml.jar/download",
+ outputTypes=['png', 'svg', 'eps', 'pdf', 'vdx', 'xmi', 'scxml', 'html', 'txt', 'utxt',
+ 'latex', 'latex:nopreamble'])
+ ]
+ return gens
+
+
+class GenerateResult(object):
+
+ def __init__(self, crc32, outputType, path, stdout, stderr):
+ """ construct me """
+ self.crc32 = crc32;
+ self.outputType = outputType;
+ self.path = path;
+ self.stdout = stdout
+ self.stderr = stderr
+
+ def errMsg(self):
+ """ decode my stdout and stderr to an error message"""
+ msg = ""
+ if self.stdout is not None:
+ msg = msg + self.stdout.decode('utf-8')
+ if self.stderr is not None:
+ msg = msg + self.stderr.decode('utf-8')
+ return msg
+
+ def asHtml(self):
+ """ return me as HTML"""
+ url = '/render/%s/%s' % (self.outputType, self.crc32)
+ if self.outputType in ['gif', 'jpg', 'png', 'svg']:
+ return "
" % url;
+ elif self.outputType in ['pdf']:
+ return "" % url;
+ else:
+ return "%s %s" % (url, self.outputType, self.crc32)
+
+ def isValid(self):
+ """ check if i am valid"""
+ valid = os.path.isfile(self.path) and not self.errMsg()
+ return valid
+
+ def asJson(self, baseurl):
+ """ return my result as JSON for the Mediawiki diagrams extension"""
+ errMsg=self.errMsg();
+ if errMsg:
+ jsonTxt="""{
+ "error": "generating %s failed",
+ "message": %s
+ }""" % (self.outputType,json.dumps(errMsg))
+ else:
+ jsonTxt = """{
+ "diagrams": {
+ "png": {
+ "url": "%s/png/%s.png"
+ }
+ }
+}""" % (baseurl,self.crc32)
+ return jsonTxt
+
+
+class Generator(object):
+ """ a diagram generator """
+
+ @staticmethod
+ def getOutputDirectory():
+ home = expanduser("~")
+ outputDir = home + "/.diagrams/"
+ if not os.path.isdir(outputDir):
+ os.mkdir(outputDir);
+ return outputDir
+
+ def __init__(self, genid, name, cmd, versionOption, logo=None, url=None, download=None, defaultType=None, aliases=None, outputTypes=None, debug=False):
+ """ construct me """
+ self.id = genid
+ self.name = name
+ self.cmd = cmd
+ self.gencmd = None
+ self.logo = logo
+ self.url = url
+ self.htmlInfo = None
+ self.download = download
+ self.versionOption = versionOption;
+ if aliases is None:
+ self.aliases = [cmd]
+ else:
+ self.aliases = aliases
+ self.defaultType = defaultType
+ self.selectedType = defaultType
+ self.outputTypes = outputTypes
+ self.debug = debug
+ pass
+
+ def getHtmlInfo(self):
+ """ get info on this generator to be displayed via HTML"""
+ # cache the info since getVersion is a costly process
+ if self.htmlInfo is None:
+ version = self.getVersion()
+ self.htmlInfo = "
" % (self.url, self.name, version, self.logo)
+ return self.htmlInfo
+
+ def check(self):
+ """ check my version"""
+ self.gencmd = Command(self.cmd, self.versionOption, debug=self.debug)
+ return self.gencmd.check()
+
+ def getVersion(self):
+ stdOutText, stdErrText = self.check()
+ if stdOutText is None:
+ stdOutText = ''
+ if stdErrText is None:
+ stdErrText = ''
+ outputText = stdOutText + stdErrText
+ found = re.search(r'version.*[,)]', outputText)
+ if found:
+ version = found.group()
+ else:
+ # actually an error message
+ version = outputText
+ # invalidate gencmd
+ self.gencmd = None
+ return version
+
+ @staticmethod
+ def getHash(txt):
+ hashValue = zlib.crc32(txt.encode()) & 0xffffffff
+ hashId = hex(hashValue)
+ #print ("txt %s\nhashValue %s\nhashId %s" % (txt, hashValue, hashId), file=sys.stderr)
+ return hashId
+
+ def wrap(self,txt):
+ """ wraot the given text"""
+ if self.id == "plantuml":
+ txt="@startuml\n%s\n@enduml\n" % txt
+ return txt
+
+ def generate(self, alias, txt, outputType, renderer, useCached=True):
+ """ generate """
+ txt=self.wrap(txt)
+ hashId = Generator.getHash(txt)
+ inputPath = "%s%s.%s" % (Generator.getOutputDirectory(), hashId, 'txt')
+ stdout = None
+ stderr = None
+ if not (os.path.isfile(inputPath) and useCached):
+ text_file = open(inputPath, "wt", encoding='utf-8')
+ try:
+ if self.debug:
+ print ("txt %s" % txt, file=sys.stderr)
+ text_file.write("%s" % txt)
+ except Exception as e:
+ print ("exception %s" % e, file=sys.stderr)
+ finally:
+ text_file.close()
+ outputPath = "%s%s.%s" % (Generator.getOutputDirectory(), hashId, outputType)
+ if os.path.isfile(outputPath) and useCached:
+ if self.debug:
+ print ("cached %s #%s from %s" % (outputType, hashId, outputPath), file=sys.stderr)
+ #else:
+ if True:
+ if self.debug:
+ print ("generating %s #%s to %s" % (outputType, hashId, outputPath), file=sys.stderr)
+ if self.id == "graphviz":
+ args = "-T%s %s -o %s" % (outputType, inputPath, outputPath)
+ if len(renderer) != 0: #exists and not empty
+ alias = renderer
+ else:
+ alias = "dot"
+ #print ("renderer %s\nalias %s" % (renderer, alias), file=sys.stderr)
+ elif self.id == "mscgen":
+ args = "-T%s -i %s -o %s" % (outputType, inputPath, outputPath)
+ elif self.id == "plantuml":
+ args = "-charset UTF-8 -t%s %s" % (outputType, inputPath)
+ alias = "java -jar /var/diagrams/plantuml.jar"
+ else:
+ print ("unknown generator %s" % self.id, file=sys.stderr)
+ if self.gencmd is None:
+ self.check()
+ stdout, stderr = self.gencmd.callalias(alias, args)
+ result = GenerateResult(hashId, outputType, outputPath, stdout, stderr)
+ return result