mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
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:
parent
2c0b81d88b
commit
6178163cb1
3
CHANGES
3
CHANGES
@ -28,6 +28,9 @@ Features added
|
|||||||
Bugs fixed
|
Bugs fixed
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* #11077: graphviz: Fix relative links from within the graph.
|
||||||
|
Patch by Ralf Grubenmann.
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
@ -6,10 +6,13 @@ from __future__ import annotations
|
|||||||
import posixpath
|
import posixpath
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
|
from itertools import chain
|
||||||
from os import path
|
from os import path
|
||||||
from subprocess import CalledProcessError
|
from subprocess import CalledProcessError
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
from docutils import nodes
|
from docutils import nodes
|
||||||
from docutils.nodes import Node
|
from docutils.nodes import Node
|
||||||
@ -214,6 +217,37 @@ class GraphvizSimple(SphinxDirective):
|
|||||||
return [figure]
|
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,
|
def render_dot(self: SphinxTranslator, code: str, options: dict, format: str,
|
||||||
prefix: str = 'graphviz', filename: str | None = None,
|
prefix: str = 'graphviz', filename: str | None = None,
|
||||||
) -> tuple[str | None, str | None]:
|
) -> tuple[str | None, str | None]:
|
||||||
@ -251,10 +285,6 @@ def render_dot(self: SphinxTranslator, code: str, options: dict, format: str,
|
|||||||
try:
|
try:
|
||||||
ret = subprocess.run(dot_args, input=code.encode(), capture_output=True,
|
ret = subprocess.run(dot_args, input=code.encode(), capture_output=True,
|
||||||
cwd=cwd, check=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:
|
except OSError:
|
||||||
logger.warning(__('dot command %r cannot be run (needed for graphviz '
|
logger.warning(__('dot command %r cannot be run (needed for graphviz '
|
||||||
'output), check the graphviz_dot setting'), graphviz_dot)
|
'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:
|
except CalledProcessError as exc:
|
||||||
raise GraphvizError(__('dot exited with error:\n[stderr]\n%r\n'
|
raise GraphvizError(__('dot exited with error:\n[stderr]\n%r\n'
|
||||||
'[stdout]\n%r') % (exc.stderr, exc.stdout)) from exc
|
'[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,
|
def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: dict,
|
||||||
|
8
tests/roots/test-ext-graphviz/_static/images/test.svg
Normal file
8
tests/roots/test-ext-graphviz/_static/images/test.svg
Normal 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 |
@ -1,2 +1,3 @@
|
|||||||
extensions = ['sphinx.ext.graphviz']
|
extensions = ['sphinx.ext.graphviz']
|
||||||
exclude_patterns = ['_build']
|
exclude_patterns = ['_build']
|
||||||
|
html_static_path = ["_static"]
|
||||||
|
@ -31,3 +31,13 @@ Hello |graph| graphviz world
|
|||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
centered
|
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
|
||||||
|
}
|
||||||
|
@ -82,6 +82,17 @@ def test_graphviz_svg_html(app, status, warning):
|
|||||||
r'</div>')
|
r'</div>')
|
||||||
assert re.search(html, content, re.S)
|
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.sphinx('latex', testroot='ext-graphviz')
|
||||||
@pytest.mark.usefixtures('if_graphviz_found')
|
@pytest.mark.usefixtures('if_graphviz_found')
|
||||||
|
Loading…
Reference in New Issue
Block a user