From 1dc7e7820cf8892b510c639b04f3411d878d0940 Mon Sep 17 00:00:00 2001 From: dimon Date: Mon, 24 Oct 2022 15:31:10 +0000 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BB(=D0=B0)=20?= =?UTF-8?q?'diagrams.py'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- diagrams.py | 315 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 diagrams.py 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