Skip to content

Commit b160c0c

Browse files
authored
Merge pull request #35 from mshroyer/rst-impl
Implement RST directive for graphviz
2 parents bb1f818 + 984fe95 commit b160c0c

File tree

6 files changed

+234
-45
lines changed

6 files changed

+234
-45
lines changed

README.md

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Graphviz: A Plugin for Pelican
66
[![Downloads](https://img.shields.io/pypi/dm/pelican-graphviz)](https://pypi.org/project/pelican-graphviz/)
77
[![License](https://img.shields.io/pypi/l/pelican-graphviz?color=blue)](https://www.gnu.org/licenses/agpl-3.0.en.html)
88

9-
Graphviz is a Pelican plugin that allows the inclusion of [Graphviz][] images using the Markdown markup format. The code for the Graphviz figure is included as a block in the article’s source. In the output HTML file, the Graphviz figure can appear as either a `<svg>` or embedded into a `<img>` element using the Base64 format.
9+
Graphviz is a Pelican plugin that allows the inclusion of [Graphviz][] images using the Markdown or reStructuredText markup format. The code for the Graphviz figure is included as a block in the article’s source. In the output HTML file, the Graphviz figure can appear as either a `<svg>` or embedded into a `<img>` element using the Base64 format.
1010

1111
[Graphviz]: https://www.graphviz.org
1212

@@ -32,9 +32,11 @@ For macOS, Graphviz can be installed via Homebrew:
3232
Usage
3333
-----
3434

35-
In the Markdown source, the Graphviz code must be inserted as an individual block (i.e., separated from the rest of the material by blank lines), like the following:
35+
### Markdown
3636

37-
```markdwon
37+
In Markdown source, the Graphviz code must be inserted as an individual block (i.e., separated from the rest of the material by blank lines), like the following:
38+
39+
```markdown
3840
..graphviz dot
3941
digraph G {
4042
graph [rankdir = LR];
@@ -50,6 +52,19 @@ The block must start with `..graphviz` (this is configurable — see below). The
5052

5153
[Graphviz documentation]: https://www.graphviz.org/documentation/
5254

55+
### reStructuredText
56+
57+
For RST input, support is implemented as a directive, with syntax like:
58+
59+
```rst
60+
.. graphviz:: dot
61+
:alt-text: My graph
62+
63+
digraph G {
64+
graph [rankdir = LR];
65+
Hello -> World
66+
}
67+
```
5368

5469
Styling with CSS
5570
----------------
@@ -83,21 +98,30 @@ The following variables can be set in the Pelican settings file:
8398

8499
- `GRAPHVIZ_IMAGE_CLASS`: Class of the `<div>` element including the generated Graphviz image (defaults to `'graphviz'`).
85100

86-
- `GRAPHVIZ_BLOCK_START`: Starting tag for the Graphviz block in Markdown (defaults to `'..graphviz'`).
101+
- `GRAPHVIZ_BLOCK_START`: Starting tag for the Graphviz block in Markdown (defaults to `'..graphviz'`). This setting has no effect for reStructuredText input.
87102

88103
- `GRAPHVIZ_COMPRESS`: Compress the resulting SVG XML to an image (defaults to `True`). Without compression, more SVG features are available, for instance including clickable URLs inside the Graphviz diagram.
89104

90105
- `GRAPHVIZ_ALT_TEXT`: String that will be used as the default value for the `alt` property of the generated `<img>` HTML element (defaults to `"[GRAPH]"`). It is only meaningful when the reuslting SVG output is compressed.
91106

92-
The values above can be overridden for each individual block using the syntax below:
107+
The values above can be overridden for each individual block using the syntax below. In Markdown, this looks like:
93108

94-
```markdwon
109+
```markdown
95110
..graphviz [key1=val1, key2="val2"...] dot
96111
```
97-
The allowed keys are `html-element`, `image-class`, `block-start`, `alt-text`, and `compress`. For the latter, the value can be either `yes` or `no`.
98112

99113
If the value needs to include a comma (`,`) or an equal sign (`=`), then use the `key2="val2"` form.
100114

115+
Or in reStructuredText:
116+
117+
```rst
118+
.. graphviz:: dot
119+
:key1: val1
120+
:key2: val2
121+
```
122+
123+
The allowed keys are `html-element`, `image-class`, `block-start`, `alt-text`, and `compress`. For the latter, the value can be either `yes` or `no`. `block-start` has no effect for reStructuredText.
124+
101125

102126
Output Image Format
103127
-------------------
@@ -130,14 +154,6 @@ An alternative to this plugin is the [Graphviz tag][] provided by the [Liquid Ta
130154
[Liquid Tags plugin]: https://github.com/pelican-plugins/liquid-tags
131155

132156

133-
To-Do
134-
-----
135-
136-
Contributions that make this plugin work with [reStructuredText][] content are welcome.
137-
138-
[reStructuredText]: https://docutils.sourceforge.io/rst.html
139-
140-
141157
Contributing
142158
------------
143159

pelican/plugins/graphviz/graphviz.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
import os
2020
import subprocess
2121

22+
from docutils.parsers.rst import directives
23+
2224
from pelican import signals
2325

2426
from .mdx_graphviz import GraphvizExtension
27+
from .rst_graphviz import make_graphviz_directive
2528

2629
logger = logging.getLogger(__name__)
2730

@@ -52,6 +55,8 @@ def initialize(pelicanobj):
5255
GraphvizExtension(config)
5356
)
5457

58+
directives.register_directive("graphviz", make_graphviz_directive(config))
59+
5560

5661
def register():
5762
"""Register the Markdown Graphviz plugin with Pelican."""

pelican/plugins/graphviz/mdx_graphviz.py

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@
1515
# You should have received a copy of the GNU Affero General Public License
1616
# along with this program. If not, see http://www.gnu.org/licenses/.
1717

18-
import base64
1918
import re
2019
import xml.etree.ElementTree as ET
2120

2221
from markdown import Extension
2322
from markdown.blockprocessors import BlockProcessor
2423

25-
from .run_graphviz import run_graphviz
24+
from .run_graphviz import append_base64_img, run_graphviz
2625

2726

2827
class GraphvizProcessor(BlockProcessor):
@@ -79,33 +78,7 @@ def run(self, parent, blocks):
7978

8079
# Cope with compression
8180
if config["compress"]:
82-
img = ET.SubElement(elt, "img")
83-
img.set(
84-
"src",
85-
"data:image/svg+xml;base64,{}".format(
86-
base64.b64encode(output).decode("ascii")
87-
),
88-
)
89-
# Set the alt text. Order of priority:
90-
# 1. Block option alt-text
91-
# 2. ID of Graphviz object
92-
# 3. Global GRAPHVIZ_ALT_TEXT option
93-
if config["alt-text"]:
94-
img.set("alt", config["alt-text"])
95-
else:
96-
m = re.search(
97-
r"<!-- Title: (.*) Pages: \d+ -->",
98-
output.decode("utf-8"),
99-
)
100-
# Gating against a matched title of "%3" works around an old
101-
# graphviz issue, which is still present in the version
102-
# shipped with Ubuntu 24.04:
103-
#
104-
# https://gitlab.com/graphviz/graphviz/-/issues/1376
105-
if m and m.group(1) != "%3":
106-
img.set("alt", m.group(1))
107-
else:
108-
img.set("alt", config["alt-text-default"])
81+
append_base64_img(output, config, elt)
10982
else:
11083
svg = output.decode()
11184
start = svg.find("<svg")
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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

pelican/plugins/graphviz/run_graphviz.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@
4646
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
4747
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4848

49+
import base64
4950
import errno
5051
import os
52+
import re
5153
from subprocess import PIPE, Popen
54+
import xml.etree.ElementTree as ET
5255

5356

5457
class DotRuntimeError(RuntimeError):
@@ -59,6 +62,40 @@ def __init__(self, errmsg):
5962
super().__init__(f"dot exited with error:\n[stderr]\n{errmsg}")
6063

6164

65+
def append_base64_img(svg: bytes, config: dict, elt: ET.Element):
66+
"""Apppend a base64 SVG img to an ElementTree element.
67+
68+
Given a binary-encoded SVG image, base64-encodes the SVG and appends it to
69+
the given element in the form of an inline img tag.
70+
71+
"""
72+
img = ET.SubElement(elt, "img")
73+
img.set(
74+
"src",
75+
"data:image/svg+xml;base64,{}".format(base64.b64encode(svg).decode("ascii")),
76+
)
77+
# Set the alt text. Order of priority:
78+
# 1. Block option alt-text
79+
# 2. ID of Graphviz object
80+
# 3. Global GRAPHVIZ_ALT_TEXT option
81+
if config["alt-text"]:
82+
img.set("alt", config["alt-text"])
83+
else:
84+
m = re.search(
85+
r"<!-- Title: (.*) Pages: \d+ -->",
86+
svg.decode("utf-8"),
87+
)
88+
# Gating against a matched title of "%3" works around an old
89+
# graphviz issue, which is still present in the version
90+
# shipped with Ubuntu 24.04:
91+
#
92+
# https://gitlab.com/graphviz/graphviz/-/issues/1376
93+
if m and m.group(1) != "%3":
94+
img.set("alt", m.group(1))
95+
else:
96+
img.set("alt", config["alt-text-default"])
97+
98+
6299
def run_graphviz(program, code, options=None, format="png"):
63100
"""Run graphviz program and returns image data."""
64101
if not options:

0 commit comments

Comments
 (0)