paul@101 | 1 | #!/usr/bin/env python |
paul@101 | 2 | |
paul@101 | 3 | """ |
paul@101 | 4 | Graphviz serialiser, generating content for embedding in HTML documents. |
paul@101 | 5 | |
paul@331 | 6 | Copyright (C) 2018, 2019, 2022, 2023 Paul Boddie <paul@boddie.org.uk> |
paul@101 | 7 | |
paul@101 | 8 | This program is free software; you can redistribute it and/or modify it under |
paul@101 | 9 | the terms of the GNU General Public License as published by the Free Software |
paul@101 | 10 | Foundation; either version 3 of the License, or (at your option) any later |
paul@101 | 11 | version. |
paul@101 | 12 | |
paul@101 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT |
paul@101 | 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
paul@101 | 15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
paul@101 | 16 | details. |
paul@101 | 17 | |
paul@101 | 18 | You should have received a copy of the GNU General Public License along with |
paul@101 | 19 | this program. If not, see <http://www.gnu.org/licenses/>. |
paul@101 | 20 | """ |
paul@101 | 21 | |
paul@101 | 22 | from moinformat.serialisers.common import Serialiser, escape_attr, escape_text |
paul@209 | 23 | from moinformat.utils.graphviz import Graphviz, IMAGE_FORMATS, \ |
paul@101 | 24 | get_output_identifier |
paul@281 | 25 | from moinformat.utils.links import LinkTarget |
paul@101 | 26 | |
paul@101 | 27 | # Utility functions. |
paul@101 | 28 | |
paul@101 | 29 | def select_keys(d, keys): |
paul@101 | 30 | |
paul@101 | 31 | "Select from 'd' the given 'keys'." |
paul@101 | 32 | |
paul@101 | 33 | if not d: |
paul@201 | 34 | return {} |
paul@101 | 35 | |
paul@101 | 36 | out = {} |
paul@101 | 37 | |
paul@101 | 38 | for key in keys: |
paul@101 | 39 | if d.has_key(key): |
paul@101 | 40 | out[key] = d[key] |
paul@101 | 41 | |
paul@101 | 42 | return out |
paul@101 | 43 | |
paul@101 | 44 | |
paul@101 | 45 | |
paul@101 | 46 | # The serialiser class. |
paul@101 | 47 | |
paul@101 | 48 | class HTMLGraphvizSerialiser(Serialiser): |
paul@101 | 49 | |
paul@101 | 50 | "Serialisation of Graphviz regions." |
paul@101 | 51 | |
paul@301 | 52 | input_formats = ["graphviz", "dot"] |
paul@301 | 53 | formats = ["html"] |
paul@301 | 54 | |
paul@101 | 55 | def init(self): |
paul@101 | 56 | self.directives = {} |
paul@101 | 57 | |
paul@101 | 58 | def start_block(self): |
paul@101 | 59 | pass |
paul@101 | 60 | |
paul@101 | 61 | def end_block(self): |
paul@101 | 62 | pass |
paul@101 | 63 | |
paul@331 | 64 | def directive(self, directive): |
paul@331 | 65 | if not self.directives.has_key(directive.key): |
paul@331 | 66 | self.directives[directive.key] = [] |
paul@331 | 67 | self.directives[directive.key].append(directive.value) |
paul@101 | 68 | |
paul@101 | 69 | def text(self, text): |
paul@331 | 70 | self.process_graph(text.s) |
paul@101 | 71 | |
paul@101 | 72 | |
paul@101 | 73 | |
paul@101 | 74 | # Special methods for graph production. |
paul@101 | 75 | |
paul@144 | 76 | def _tag(self, tagname, attrname, target, attributes, closing): |
paul@144 | 77 | l = ["%s='%s'" % (attrname, escape_attr(target))] |
paul@101 | 78 | for key, value in attributes.items(): |
paul@101 | 79 | l.append("%s='%s'" % (key, value)) |
paul@153 | 80 | self.out("<%s %s%s>" % (tagname, " ".join(l), closing and " /" or "")) |
paul@101 | 81 | |
paul@144 | 82 | def image(self, target, attributes): |
paul@144 | 83 | self._tag("img", "src", target, attributes, True) |
paul@101 | 84 | |
paul@144 | 85 | def object(self, target, attributes): |
paul@144 | 86 | self._tag("object", "data", target, attributes, False) |
paul@101 | 87 | self.out("</object>") |
paul@101 | 88 | |
paul@101 | 89 | def raw(self, text): |
paul@101 | 90 | self.out(text) |
paul@101 | 91 | |
paul@101 | 92 | |
paul@101 | 93 | |
paul@101 | 94 | # Graph output preparation. |
paul@101 | 95 | |
paul@101 | 96 | def process_graph(self, text): |
paul@101 | 97 | |
paul@101 | 98 | "Process the graph 'text' using the known directives." |
paul@101 | 99 | |
paul@101 | 100 | filter = self.directives.get("filter", ["dot"])[0] |
paul@101 | 101 | format = self.directives.get("format", ["svg"])[0] |
paul@101 | 102 | transforms = self.directives.get("transform", []) |
paul@101 | 103 | |
paul@264 | 104 | # Add transforms if appropriate. |
paul@264 | 105 | # NOTE: Maybe move this to the Graphviz class itself. |
paul@264 | 106 | |
paul@264 | 107 | document_index = self.metadata.get("document_index") |
paul@264 | 108 | |
paul@264 | 109 | if document_index and "fixlinks" not in transforms: |
paul@264 | 110 | transforms.insert(0, "fixlinks") |
paul@264 | 111 | |
paul@264 | 112 | # Determine the inline status, with only SVG being appropriate. |
paul@264 | 113 | |
paul@282 | 114 | inline = format == "svg" and not self.metadata.get("no_inline") |
paul@189 | 115 | |
paul@189 | 116 | # Non-inline graph output is stored for a known page only. |
paul@144 | 117 | |
paul@165 | 118 | pagename = self.metadata.get("pagename") |
paul@189 | 119 | |
paul@189 | 120 | if not inline: |
paul@189 | 121 | if not pagename: |
paul@189 | 122 | return |
paul@144 | 123 | |
paul@189 | 124 | # Get an identifier and usable filename to store the output. |
paul@101 | 125 | |
paul@189 | 126 | identifier = get_output_identifier(text) |
paul@189 | 127 | attachment = "%s.%s" % (identifier, format) |
paul@189 | 128 | filename = self.output.get_attachment_filename(pagename, attachment) |
paul@189 | 129 | |
paul@189 | 130 | # Handle situations where no independent output is permitted. |
paul@101 | 131 | |
paul@189 | 132 | if not filename: |
paul@189 | 133 | return |
paul@104 | 134 | |
paul@189 | 135 | # Make sure that page attachments can be stored. |
paul@104 | 136 | |
paul@189 | 137 | self.output.ensure_attachments(pagename) |
paul@319 | 138 | link = self.linker.translate(LinkTarget("attachment", attachment, |
paul@319 | 139 | pagename=pagename)) |
paul@144 | 140 | |
paul@189 | 141 | # No filename is defined for inline output. |
paul@189 | 142 | |
paul@189 | 143 | else: |
paul@189 | 144 | filename = None |
paul@144 | 145 | |
paul@101 | 146 | # Permit imagemaps only for image formats. |
paul@101 | 147 | |
paul@101 | 148 | if format in IMAGE_FORMATS: |
paul@101 | 149 | cmapx = self.directives.has_key("cmapx") |
paul@101 | 150 | |
paul@101 | 151 | # Configure Graphviz and invoke it. |
paul@101 | 152 | |
paul@189 | 153 | graphviz = Graphviz(filter, text) |
paul@264 | 154 | graphviz.call(format, transforms, filename, self.metadata) |
paul@101 | 155 | |
paul@101 | 156 | # Obtain any metadata. |
paul@101 | 157 | |
paul@101 | 158 | attributes = select_keys(graphviz.get_metadata(), ["width", "height"]) |
paul@101 | 159 | |
paul@101 | 160 | # For image output, create a file directly and reference it. |
paul@101 | 161 | |
paul@101 | 162 | if format in IMAGE_FORMATS: |
paul@101 | 163 | |
paul@101 | 164 | # Produce, embed and reference an imagemap if requested. |
paul@101 | 165 | |
paul@101 | 166 | if cmapx: |
paul@101 | 167 | graphviz.call("cmapx") |
paul@101 | 168 | mapid = graphviz.get_metadata().get("id") |
paul@101 | 169 | |
paul@101 | 170 | if mapid: |
paul@101 | 171 | self.raw(graphviz.get_output()) |
paul@101 | 172 | attributes["usemap"] = "#%s" % im_attributes["id"] |
paul@101 | 173 | |
paul@214 | 174 | self.image(link.get_target(), attributes) |
paul@101 | 175 | |
paul@101 | 176 | # For other output, create a file and embed the object. |
paul@101 | 177 | |
paul@189 | 178 | elif not inline: |
paul@214 | 179 | self.object(link.get_target(), attributes) |
paul@189 | 180 | |
paul@189 | 181 | # Or for inline output, emit it in the document itself. |
paul@189 | 182 | |
paul@101 | 183 | else: |
paul@189 | 184 | self.out(graphviz.get_inline_output()) |
paul@101 | 185 | |
paul@101 | 186 | serialiser = HTMLGraphvizSerialiser |
paul@101 | 187 | |
paul@101 | 188 | # vim: tabstop=4 expandtab shiftwidth=4 |