# HG changeset patch # User Paul Boddie # Date 1532899693 -7200 # Node ID f92b6c71b4bab3c7c69237bb82af47f3b98379a8 # Parent cd1788f92058413d63c92f8e5cbf37a91b02a7ea Added support for Graphviz regions, storing output using the configured context. diff -r cd1788f92058 -r f92b6c71b4ba moinformat/parsers/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/parsers/graphviz.py Sun Jul 29 23:28:13 2018 +0200 @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +""" +Graphviz region metadata parser. This only identifies metadata, with the actual +graph data being interpreted by Graphviz itself. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from moinformat.parsers.common import ParserBase, get_patterns, group, optional +from moinformat.parsers.moin import MoinParser +from moinformat.tree.graphviz import Directive +from moinformat.tree.moin import Text + +join = "".join + +# Parser functionality. + +class GraphvizParser(ParserBase): + + "A parser for Graphviz content, identifying format directives." + + # Parser handler methods. + + def parse_directive(self, region): + + "Handle format directives." + + key = self.match_group("key") + value = self.match_group("value") + self.add_node(region, Directive(key, value)) + self.new_block(region) + + + + # Regular expressions. + + syntax = { + # At start of line: + + "directive" : join(("^//", # // + group("key", ".*?"), # text-excl-eq-nl + optional(join(("=", # eq (optional) + group("value", ".*?")))), # text-excl-nl (optional) + "\n")), # nl + + "regionend" : MoinParser.syntax["regionend"], + } + + patterns = get_patterns(syntax) + + + + # Pattern details. + + region_pattern_names = ["directive", "regionend"] + + + + # Pattern handlers. + + parse_region_end = ParserBase.parse_region_end + + handlers = { + "directive" : parse_directive, + "regionend" : parse_region_end, + } + +parser = GraphvizParser + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r cd1788f92058 -r f92b6c71b4ba moinformat/serialisers/html/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/serialisers/html/graphviz.py Sun Jul 29 23:28:13 2018 +0200 @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +""" +Graphviz serialiser, generating content for embedding in HTML documents. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from moinformat.serialisers.common import Serialiser, escape_attr, escape_text +from moinformat.utils.graphviz import Graphviz, GraphvizError, IMAGE_FORMATS, \ + get_output_identifier + +# Utility functions. + +def select_keys(d, keys): + + "Select from 'd' the given 'keys'." + + if not d: + return [] + + out = {} + + for key in keys: + if d.has_key(key): + out[key] = d[key] + + return out + + + +# The serialiser class. + +class HTMLGraphvizSerialiser(Serialiser): + + "Serialisation of Graphviz regions." + + def init(self): + self.directives = {} + + def start_block(self): + pass + + def end_block(self): + pass + + def directive(self, key, value): + if not self.directives.has_key(key): + self.directives[key] = [] + self.directives[key].append(value) + + def text(self, text): + self.process_graph(text) + + + + # Special methods for graph production. + + def _tag(self, tagname, attrname, filename, attributes, closing): + l = ["%s='%s'" % (attrname, escape_attr(filename))] + for key, value in attributes.items(): + l.append("%s='%s'" % (key, value)) + self.out("<%s %s%s>" % (tagname, " ".join(l), closing and " /")) + + def image(self, filename, attributes): + self._tag("img", "src", filename, attributes, True) + + def object(self, filename, attributes): + self._tag("object", "data", filename, attributes, False) + self.out("") + + def raw(self, text): + self.out(text) + + + + # Graph output preparation. + + def process_graph(self, text): + + "Process the graph 'text' using the known directives." + + filter = self.directives.get("filter", ["dot"])[0] + format = self.directives.get("format", ["svg"])[0] + transforms = self.directives.get("transform", []) + + # Get an identifier and usable filename to store the output. + + identifier = get_output_identifier(text) + filename = self.output.get_filename(identifier) + + # Permit imagemaps only for image formats. + + if format in IMAGE_FORMATS: + cmapx = self.directives.has_key("cmapx") + + # Configure Graphviz and invoke it. + + graphviz = Graphviz(filter, text, identifier) + graphviz.call(format, transforms, filename) + + # Obtain any metadata. + + attributes = select_keys(graphviz.get_metadata(), ["width", "height"]) + + # For image output, create a file directly and reference it. + + if format in IMAGE_FORMATS: + + # Produce, embed and reference an imagemap if requested. + + if cmapx: + graphviz.call("cmapx") + mapid = graphviz.get_metadata().get("id") + + if mapid: + self.raw(graphviz.get_output()) + attributes["usemap"] = "#%s" % im_attributes["id"] + + self.image(filename, attributes) + + # For other output, create a file and embed the object. + + else: + self.object(filename, attributes) + +serialiser = HTMLGraphvizSerialiser + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r cd1788f92058 -r f92b6c71b4ba moinformat/serialisers/moin/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/serialisers/moin/graphviz.py Sun Jul 29 23:28:13 2018 +0200 @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +""" +Moin Graphviz region serialiser. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from moinformat.serialisers.common import Serialiser + +class MoinGraphvizSerialiser(Serialiser): + + "Serialisation of the page." + + def start_block(self): + pass + + def end_block(self): + pass + + def directive(self, key, value): + self.out("//%s%s\n" % (value and "%s=" % key or key, value or "")) + + def text(self, text): + self.out(text) + +serialiser = MoinGraphvizSerialiser + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r cd1788f92058 -r f92b6c71b4ba moinformat/tree/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/tree/graphviz.py Sun Jul 29 23:28:13 2018 +0200 @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +""" +Graphviz document tree nodes. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from moinformat.tree.moin import Node + +class Directive(Node): + + "Format directive for Graphviz output." + + def __init__(self, key=None, value=None): + self.key = key + self.value = value + + def __repr__(self): + return "Directive(%r, %r)" % (self.key, self.value) + + def prettyprint(self, indent=""): + return "%sDirective: key=%r value=%r" % (indent, self.key, self.value) + + def to_string(self, out): + out.directive(self.key, self.value) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r cd1788f92058 -r f92b6c71b4ba moinformat/utils/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/utils/__init__.py Sun Jul 29 23:28:13 2018 +0200 @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +""" +Utilities. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r cd1788f92058 -r f92b6c71b4ba moinformat/utils/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/utils/graphviz.py Sun Jul 29 23:28:13 2018 +0200 @@ -0,0 +1,267 @@ +#!/usr/bin/env python + +""" +Graphviz utilities. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from os.path import exists, join +from StringIO import StringIO +from subprocess import Popen, PIPE +import gzip +import sha +import xml.sax + +# Configurable paths and locations. + +DIAGRAM_TOOLS_PATH = "/home/paulb/Software/Graphical/diagram-tools" +GRAPHVIZ_PATH = "/usr/bin" +XSLT_PROCESSOR = "/usr/bin/xsltproc" + +# Graphviz "filter" programs performing layout. + +FILTERS = ['circo', 'dot', 'fdp', 'neato', 'twopi'] + +# Supported output formats. + +IMAGE_FORMATS = ['png', 'gif'] +SVG_FORMATS = ['svg', 'svgz'] + +OUTPUT_FORMATS = IMAGE_FORMATS + SVG_FORMATS + \ + ['dia', 'fig', 'hpgl', 'imap', 'mif', 'pcl', 'ps'] + +# XSL transformations for SVG output. + +TRANSFORMS = { + "notugly" : join(DIAGRAM_TOOLS_PATH, "notugly.xsl"), + } + + + +# Utility functions. + +def encode(s, encoding): + + "Encode 's' using 'encoding' if Unicode." + + if isinstance(s, unicode): + return s.encode(encoding) + else: + return s + +class MetadataParser(xml.sax.handler.ContentHandler): + + "Parse metadata from the svg element." + + def __init__(self): + self.attrs = {} + + def startElement(self, name, attrs): + if name == self.tagname: + self.attrs = dict(attrs) + + def parse(self, f): + + "Parse content from the file object 'f' using reasonable defaults." + + try: + parser = xml.sax.make_parser() + parser.setContentHandler(self) + parser.setErrorHandler(xml.sax.handler.ErrorHandler()) + parser.setFeature(xml.sax.handler.feature_external_ges, 0) + parser.parse(f) + finally: + f.close() + + def get_metadata(self, data, tagname): + + "Process 'data', returning attributes from 'tagname'." + + self.tagname = tagname + + f = StringIO(data) + try: + self.parse(f) + finally: + f.close() + + return self.attrs + +def get_output_identifier(text): + + "Return an output identifier for the given 'text'." + + return sha.new(encode(text, 'utf-8')).hexdigest() + +def get_program(filter): + + "Return the program for the given 'filter'." + + if not filter in FILTERS: + return None + else: + return join(GRAPHVIZ_PATH, filter) + +def transform_output(process, format, transforms): + + "Transform the output from 'process' as 'format' using 'transforms'." + + # No transformation can occur if the processor is missing. + + if not exists(XSLT_PROCESSOR): + return process + + # Chain transformation processors, each accepting the output of the + # preceding one, with the first accepting the initial Graphviz output. + + for transform in transforms: + stylesheet = TRANSFORMS.get(transform) + + # Ignore unrecognised or missing stylesheets. + + if not stylesheet or not exists(stylesheet): + continue + + # Invoke the processor, indicating standard input as the source + # document. + # Example: /usr/bin/dot /usr/local/share/diagram-tools/notugly.xsl - + + process = Popen( + [XSLT_PROCESSOR, stylesheet, "-"], + shell=False, + stdin=process.stdout, + stdout=PIPE, + stderr=PIPE, + close_fds=True) + + return process + +def writefile(s, filename, compressed=False): + + "Write 's' to the file having 'filename'." + + if compressed: + f = gzip.open(filename, "w") + else: + f = open(filename, "w") + + try: + f.write(s) + finally: + f.close() + + + +# Classes for interacting with Graphviz. + +class GraphvizError(Exception): + + "An error produced when using Graphviz." + + def __init__(self, errors): + self.errors = errors + +class Graphviz: + + "A Graphviz configuration for single or repeated invocation." + + def __init__(self, filter, text, identifier): + + """ + Employ the given 'filter' to produce a graph from the given 'text'. The + output 'identifier' for the text is used to provide a filename, if + required. + """ + + self.filter = filter + self.text = text + self.identifier = identifier + + def call(self, format, transforms=None, filename=None): + + """ + Invoke Graphviz to produce output in the given 'format'. Any + 'transforms' are used to transform the output, if appropriate. Any + given 'filename' is used to write to a file. + """ + + program = get_program(self.filter) + + # Generate uncompressed SVG for later compression. + + graphviz_format = format == "svgz" and "svg" or format + + # Indicate a filename for direct output for non-SVG formats. + + svg = format in SVG_FORMATS + options = filename and not svg and ["-o", filename] or [] + + # Invoke the layout program, with the text to be provided on its + # standard input. + # Example: /usr/bin/dot -Tsvg -o filename + + start = end = Popen( + [program, '-T%s' % graphviz_format] + options, + shell=False, + stdin=PIPE, + stdout=PIPE, + stderr=PIPE) + + # Chain the invocation to transformations, if appropriate. + + if svg and transforms: + end = transform_output(start, format, transforms) + + # Send the graph to the filter. + + start.stdin.write(encode(self.text, 'utf-8')) + + if end is not start: + start.stdin.close() + + # Obtain the eventual output. + + (self.output, errors) = end.communicate() + + # Obtain any metadata. + + if svg: + parser = MetadataParser() + self.metadata = parser.get_metadata(self.output, "svg") + elif format == "cmapx": + parser = MetadataParser() + self.metadata = parser.get_metadata(self.output, "map") + else: + self.metadata = {} + + # Test for errors. + + if end.wait() != 0: + raise GraphvizError, errors + + # Write the file separately, if requested. + + if svg and filename: + writefile(self.get_output(), filename, format == "svgz") + + def get_metadata(self): + return self.metadata + + def get_output(self): + return self.output + +# vim: tabstop=4 expandtab shiftwidth=4