Fix relative references in SVGs generated by `sphinx.ext.graphviz` (#11078)

Co-authored-by: Ralf Grubenmann <ralf.grubenmann@gmail.com>
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
Ralf Grubenmann 2023-07-28 06:47:23 +02:00 committed by GitHub
parent 2c0b81d88b
commit 6178163cb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 75 additions and 4 deletions

View File

@ -28,6 +28,9 @@ Features added
Bugs fixed
----------
* #11077: graphviz: Fix relative links from within the graph.
Patch by Ralf Grubenmann.
Testing
-------

View File

@ -6,10 +6,13 @@ from __future__ import annotations
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
@ -214,6 +217,37 @@ class GraphvizSimple(SphinxDirective):
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]:
@ -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)
@ -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,

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1"
height="128" width="128"
xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="red" />
</svg>

After

Width:  |  Height:  |  Size: 187 B

View File

@ -1,2 +1,3 @@
extensions = ['sphinx.ext.graphviz']
exclude_patterns = ['_build']
html_static_path = ["_static"]

View File

@ -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
}

View File

@ -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')