Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Graphviz: A Plugin for Pelican
[![Downloads](https://img.shields.io/pypi/dm/pelican-graphviz)](https://pypi.org/project/pelican-graphviz/)
[![License](https://img.shields.io/pypi/l/pelican-graphviz?color=blue)](https://www.gnu.org/licenses/agpl-3.0.en.html)

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.
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.

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

Expand All @@ -32,9 +32,11 @@ For macOS, Graphviz can be installed via Homebrew:
Usage
-----

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:
### Markdown

```markdwon
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:

```markdown
..graphviz dot
digraph G {
graph [rankdir = LR];
Expand All @@ -50,6 +52,19 @@ The block must start with `..graphviz` (this is configurable — see below). The

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

### reStructuredText

For RST input, support is implemented as a directive, with syntax like:

```rst
.. graphviz:: dot
:alt-text: My graph

digraph G {
graph [rankdir = LR];
Hello -> World
}
```

Styling with CSS
----------------
Expand Down Expand Up @@ -83,21 +98,30 @@ The following variables can be set in the Pelican settings file:

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

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

- `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.

- `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.

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

```markdwon
```markdown
..graphviz [key1=val1, key2="val2"...] dot
```
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`.

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

Or in reStructuredText:

```rst
.. graphviz:: dot
:key1: val1
:key2: val2
```

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.


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


To-Do
-----

Contributions that make this plugin work with [reStructuredText][] content are welcome.

[reStructuredText]: https://docutils.sourceforge.io/rst.html


Contributing
------------

Expand Down
5 changes: 5 additions & 0 deletions pelican/plugins/graphviz/graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import os
import subprocess

from docutils.parsers.rst import directives

from pelican import signals

from .mdx_graphviz import GraphvizExtension
from .rst_graphviz import make_graphviz_directive

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -52,6 +55,8 @@ def initialize(pelicanobj):
GraphvizExtension(config)
)

directives.register_directive("graphviz", make_graphviz_directive(config))


def register():
"""Register the Markdown Graphviz plugin with Pelican."""
Expand Down
31 changes: 2 additions & 29 deletions pelican/plugins/graphviz/mdx_graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see http://www.gnu.org/licenses/.

import base64
import re
import xml.etree.ElementTree as ET

from markdown import Extension
from markdown.blockprocessors import BlockProcessor

from .run_graphviz import run_graphviz
from .run_graphviz import append_base64_img, run_graphviz


class GraphvizProcessor(BlockProcessor):
Expand Down Expand Up @@ -79,33 +78,7 @@ def run(self, parent, blocks):

# Cope with compression
if config["compress"]:
img = ET.SubElement(elt, "img")
img.set(
"src",
"data:image/svg+xml;base64,{}".format(
base64.b64encode(output).decode("ascii")
),
)
# Set the alt text. Order of priority:
# 1. Block option alt-text
# 2. ID of Graphviz object
# 3. Global GRAPHVIZ_ALT_TEXT option
if config["alt-text"]:
img.set("alt", config["alt-text"])
else:
m = re.search(
r"<!-- Title: (.*) Pages: \d+ -->",
output.decode("utf-8"),
)
# Gating against a matched title of "%3" works around an old
# graphviz issue, which is still present in the version
# shipped with Ubuntu 24.04:
#
# https://gitlab.com/graphviz/graphviz/-/issues/1376
if m and m.group(1) != "%3":
img.set("alt", m.group(1))
else:
img.set("alt", config["alt-text-default"])
append_base64_img(output, config, elt)
else:
svg = output.decode()
start = svg.find("<svg")
Expand Down
91 changes: 91 additions & 0 deletions pelican/plugins/graphviz/rst_graphviz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""reStructuredText extension for the Graphviz plugin for Pelican."""

# Copyright (C) 2025 Mark Shroyer <mark@shroyer.name>
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Affero 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
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see http://www.gnu.org/licenses/.

import html
from typing import ClassVar
import xml.etree.ElementTree as ET

from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.parsers.rst.directives import unchanged

from .run_graphviz import append_base64_img, run_graphviz


def truthy(argument: str) -> bool:
"""Parse a "truthy" RST option.

Applies permissive conventions to interpret "truthy"-looking strings as
True, or False otherwise.

"""
return argument.lower() in ("yes", "true", "on", "1")


def make_graphviz_directive(base_config: dict):
"""Make a graphviz RST directive incorporating the plugin's configuration.

Returns a Directive subclass that implements graphviz support, taking into
account the plugin's runtime configuration.

"""

class GraphvizDirective(Directive):
"""An RST directive for embedded Graphviz.

Takes the name of a graphviz program, such as dot, as a required
argument, and invokes it over the directive's content. The plugin's
configuration can be overridden using directive options.

"""

required_arguments = 1
option_spec: ClassVar = {
"image-class": unchanged,
"html-element": unchanged,
"compress": truthy,
"alt-text": unchanged,
}
has_content = True

def run(self):
config = base_config.copy()
config.update(self.options)

program = self.arguments[0]
code = "\n".join(self.content)

output = run_graphviz(program, code, format="svg")

elt = ET.Element(config["html-element"])
elt.set("class", config["image-class"])

if config["compress"]:
append_base64_img(output, config, elt)
img_html = ET.tostring(elt, encoding="unicode", method="html")
else:
svg = output.decode()
start = svg.find("<svg")
tag = html.escape(config["html-element"], quote=True)
class_ = html.escape(config["image-class"], quote=True)
img_html = f'<{tag} class="{class_}">{svg[start:]}</{tag}>'

svg_node = nodes.raw("", img_html, format="html")
container = nodes.container("", svg_node, classes=["graphviz"])
return [container]

return GraphvizDirective
37 changes: 37 additions & 0 deletions pelican/plugins/graphviz/run_graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import base64
import errno
import os
import re
from subprocess import PIPE, Popen
import xml.etree.ElementTree as ET


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


def append_base64_img(svg: bytes, config: dict, elt: ET.Element):
"""Apppend a base64 SVG img to an ElementTree element.

Given a binary-encoded SVG image, base64-encodes the SVG and appends it to
the given element in the form of an inline img tag.

"""
img = ET.SubElement(elt, "img")
img.set(
"src",
"data:image/svg+xml;base64,{}".format(base64.b64encode(svg).decode("ascii")),
)
# Set the alt text. Order of priority:
# 1. Block option alt-text
# 2. ID of Graphviz object
# 3. Global GRAPHVIZ_ALT_TEXT option
if config["alt-text"]:
img.set("alt", config["alt-text"])
else:
m = re.search(
r"<!-- Title: (.*) Pages: \d+ -->",
svg.decode("utf-8"),
)
# Gating against a matched title of "%3" works around an old
# graphviz issue, which is still present in the version
# shipped with Ubuntu 24.04:
#
# https://gitlab.com/graphviz/graphviz/-/issues/1376
if m and m.group(1) != "%3":
img.set("alt", m.group(1))
else:
img.set("alt", config["alt-text-default"])


def run_graphviz(program, code, options=None, format="png"):
"""Run graphviz program and returns image data."""
if not options:
Expand Down
Loading