|
| 1 | +"""reStructuredText extension for the Graphviz plugin for Pelican.""" |
| 2 | + |
| 3 | +# Copyright (C) 2025 Mark Shroyer <[email protected]> |
| 4 | +# |
| 5 | +# This program is free software: you can redistribute it and/or modify it |
| 6 | +# under the terms of the GNU General Affero Public License as published by |
| 7 | +# the Free Software Foundation, either version 3 of the License, or (at |
| 8 | +# your option) any later version. |
| 9 | +# |
| 10 | +# This program is distributed in the hope that it will be useful, but |
| 11 | +# WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 13 | +# Affero General Public License for more details. |
| 14 | +# |
| 15 | +# You should have received a copy of the GNU Affero General Public License |
| 16 | +# along with this program. If not, see http://www.gnu.org/licenses/. |
| 17 | + |
| 18 | +import html |
| 19 | +from typing import ClassVar |
| 20 | +import xml.etree.ElementTree as ET |
| 21 | + |
| 22 | +from docutils import nodes |
| 23 | +from docutils.parsers.rst import Directive |
| 24 | +from docutils.parsers.rst.directives import unchanged |
| 25 | + |
| 26 | +from .run_graphviz import append_base64_img, run_graphviz |
| 27 | + |
| 28 | + |
| 29 | +def truthy(argument: str) -> bool: |
| 30 | + """Parse a "truthy" RST option. |
| 31 | +
|
| 32 | + Applies permissive conventions to interpret "truthy"-looking strings as |
| 33 | + True, or False otherwise. |
| 34 | +
|
| 35 | + """ |
| 36 | + return argument.lower() in ("yes", "true", "on", "1") |
| 37 | + |
| 38 | + |
| 39 | +def make_graphviz_directive(base_config: dict): |
| 40 | + """Make a graphviz RST directive incorporating the plugin's configuration. |
| 41 | +
|
| 42 | + Returns a Directive subclass that implements graphviz support, taking into |
| 43 | + account the plugin's runtime configuration. |
| 44 | +
|
| 45 | + """ |
| 46 | + |
| 47 | + class GraphvizDirective(Directive): |
| 48 | + """An RST directive for embedded Graphviz. |
| 49 | +
|
| 50 | + Takes the name of a graphviz program, such as dot, as a required |
| 51 | + argument, and invokes it over the directive's content. The plugin's |
| 52 | + configuration can be overridden using directive options. |
| 53 | +
|
| 54 | + """ |
| 55 | + |
| 56 | + required_arguments = 1 |
| 57 | + option_spec: ClassVar = { |
| 58 | + "image-class": unchanged, |
| 59 | + "html-element": unchanged, |
| 60 | + "compress": truthy, |
| 61 | + "alt-text": unchanged, |
| 62 | + } |
| 63 | + has_content = True |
| 64 | + |
| 65 | + def run(self): |
| 66 | + config = base_config.copy() |
| 67 | + config.update(self.options) |
| 68 | + |
| 69 | + program = self.arguments[0] |
| 70 | + code = "\n".join(self.content) |
| 71 | + |
| 72 | + output = run_graphviz(program, code, format="svg") |
| 73 | + |
| 74 | + elt = ET.Element(config["html-element"]) |
| 75 | + elt.set("class", config["image-class"]) |
| 76 | + |
| 77 | + if config["compress"]: |
| 78 | + append_base64_img(output, config, elt) |
| 79 | + img_html = ET.tostring(elt, encoding="unicode", method="html") |
| 80 | + else: |
| 81 | + svg = output.decode() |
| 82 | + start = svg.find("<svg") |
| 83 | + tag = html.escape(config["html-element"], quote=True) |
| 84 | + class_ = html.escape(config["image-class"], quote=True) |
| 85 | + img_html = f'<{tag} class="{class_}">{svg[start:]}</{tag}>' |
| 86 | + |
| 87 | + svg_node = nodes.raw("", img_html, format="html") |
| 88 | + container = nodes.container("", svg_node, classes=["graphviz"]) |
| 89 | + return [container] |
| 90 | + |
| 91 | + return GraphvizDirective |
0 commit comments