Skip to content

Commit

Permalink
Fix relative references in SVGs generated by sphinx.ext.graphviz (#…
Browse files Browse the repository at this point in the history
…11078)

Co-authored-by: Ralf Grubenmann <ralf.grubenmann@gmail.com>
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 28, 2023
1 parent 2c0b81d commit 6178163
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 4 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -28,6 +28,9 @@ Features added
Bugs fixed
----------

* #11077: graphviz: Fix relative links from within the graph.
Patch by Ralf Grubenmann.

Testing
-------

Expand Down
46 changes: 42 additions & 4 deletions sphinx/ext/graphviz.py
Expand Up @@ -6,10 +6,13 @@
import posixpath
import re
import subprocess
import xml.etree.ElementTree as ET
from hashlib import sha1
from itertools import chain
from os import path
from subprocess import CalledProcessError
from typing import TYPE_CHECKING, Any
from urllib.parse import urlsplit, urlunsplit

from docutils import nodes
from docutils.nodes import Node
Expand Down Expand Up @@ -214,6 +217,37 @@ def run(self) -> list[Node]:
return [figure]


def fix_svg_relative_paths(self: SphinxTranslator, filepath: str) -> None:
"""Change relative links in generated svg files to be relative to imgpath."""
tree = ET.parse(filepath) # NoQA: S314
root = tree.getroot()
ns = {'svg': 'http://www.w3.org/2000/svg', 'xlink': 'http://www.w3.org/1999/xlink'}
href_name = '{http://www.w3.org/1999/xlink}href'
modified = False

for element in chain(
root.findall('.//svg:image[@xlink:href]', ns),
root.findall('.//svg:a[@xlink:href]', ns),
):
scheme, hostname, url, query, fragment = urlsplit(element.attrib[href_name])
if hostname:
# not a relative link
continue

old_path = path.join(self.builder.outdir, url)
new_path = path.relpath(
old_path,
start=path.join(self.builder.outdir, self.builder.imgpath),
)
modified_url = urlunsplit((scheme, hostname, new_path, query, fragment))

element.set(href_name, modified_url)
modified = True

if modified:
tree.write(filepath)


def render_dot(self: SphinxTranslator, code: str, options: dict, format: str,
prefix: str = 'graphviz', filename: str | None = None,
) -> tuple[str | None, str | None]:
Expand Down Expand Up @@ -251,10 +285,6 @@ def render_dot(self: SphinxTranslator, code: str, options: dict, format: str,
try:
ret = subprocess.run(dot_args, input=code.encode(), capture_output=True,
cwd=cwd, check=True)
if not path.isfile(outfn):
raise GraphvizError(__('dot did not produce an output file:\n[stderr]\n%r\n'
'[stdout]\n%r') % (ret.stderr, ret.stdout))
return relfn, outfn
except OSError:
logger.warning(__('dot command %r cannot be run (needed for graphviz '
'output), check the graphviz_dot setting'), graphviz_dot)
Expand All @@ -265,6 +295,14 @@ def render_dot(self: SphinxTranslator, code: str, options: dict, format: str,
except CalledProcessError as exc:
raise GraphvizError(__('dot exited with error:\n[stderr]\n%r\n'
'[stdout]\n%r') % (exc.stderr, exc.stdout)) from exc
if not path.isfile(outfn):
raise GraphvizError(__('dot did not produce an output file:\n[stderr]\n%r\n'
'[stdout]\n%r') % (ret.stderr, ret.stdout))

if format == 'svg':
fix_svg_relative_paths(self, outfn)

return relfn, outfn


def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: dict,
Expand Down
8 changes: 8 additions & 0 deletions tests/roots/test-ext-graphviz/_static/images/test.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/roots/test-ext-graphviz/conf.py
@@ -1,2 +1,3 @@
extensions = ['sphinx.ext.graphviz']
exclude_patterns = ['_build']
html_static_path = ["_static"]
10 changes: 10 additions & 0 deletions tests/roots/test-ext-graphviz/index.rst
Expand Up @@ -31,3 +31,13 @@ Hello |graph| graphviz world
:align: center

centered

.. graphviz::
:align: center

digraph test {
foo [label="foo", URL="#graphviz", target="_parent"]
bar [label="bar", image="./_static/images/test.svg"]
baz [label="baz", URL="./_static/images/test.svg"]
foo -> bar -> baz
}
11 changes: 11 additions & 0 deletions tests/test_ext_graphviz.py
Expand Up @@ -82,6 +82,17 @@ def test_graphviz_svg_html(app, status, warning):
r'</div>')
assert re.search(html, content, re.S)

image_re = r'.*data="([^"]+)".*?digraph test'
image_path_match = re.search(image_re, content, re.S)
assert image_path_match

image_path = image_path_match.group(1)
image_content = (app.outdir / image_path).read_text(encoding='utf8')
assert '"./_static/' not in image_content
assert '<ns0:image ns1:href="../_static/images/test.svg"' in image_content
assert '<ns0:a ns1:href="../_static/images/test.svg"' in image_content
assert '<ns0:a ns1:href="..#graphviz"' in image_content


@pytest.mark.sphinx('latex', testroot='ext-graphviz')
@pytest.mark.usefixtures('if_graphviz_found')
Expand Down

0 comments on commit 6178163

Please sign in to comment.