diff --git a/CHANGES b/CHANGES index 2e4cd7796..2b2521039 100644 --- a/CHANGES +++ b/CHANGES @@ -212,6 +212,8 @@ Bugs fixed * #4477: Build fails after building specific files * #4449: apidoc: include "empty" packages that contain modules * #3917: citation labels are tranformed to ellipsis +* #4501: graphviz: epub3 validation error caused if graph is not clickable +* #4514: graphviz: workaround for wrong map ID which graphviz generates Testing -------- diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 2f8428989..2b2473e6c 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -188,6 +188,10 @@ class BuildInfo(object): return (self.config_hash == other.config_hash and self.tags_hash == other.tags_hash) + def __ne__(self, other): # type: ignore + # type: (BuildInfo) -> bool + return not (self == other) # for py27 + def dump(self, f): # type: (IO) -> None f.write('# Sphinx build info version 1\n' diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index be1843e1e..2986bf9c1 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -37,13 +37,54 @@ if False: logger = logging.getLogger(__name__) -mapname_re = re.compile(r' None + self.id = None # type: unicode + self.filename = filename + self.content = content.splitlines() + self.clickable = [] # type: List[unicode] + + self.parse(dot=dot) + + def parse(self, dot=None): + # type: (unicode) -> None + matched = self.maptag_re.match(self.content[0]) # type: ignore + if not matched: + raise GraphvizError('Invalid clickable map file found: %s' % self.filename) + + self.id = matched.group(1) + if self.id == '%3': + # graphviz generates wrong ID if graph name not specified + # https://gitlab.com/graphviz/graphviz/issues/1327 + hashed = sha1(dot.encode('utf-8')).hexdigest() + self.id = 'grapviz%s' % hashed[-10:] + self.content[0] = self.content[0].replace('%3', self.id) + + for line in self.content: + if self.href_re.search(line): # type: ignore + self.clickable.append(line) + + def generate_clickable_map(self): + # type: () -> unicode + """Generate clickable map tags if clickable item exists. + + If not exists, this only returns empty string. + """ + if self.clickable: + return '\n'.join([self.content[0]] + self.clickable + [self.content[-1]]) + else: + return '' + + class graphviz(nodes.General, nodes.Inline, nodes.Element): pass @@ -253,18 +294,17 @@ def render_dot_html(self, node, code, options, prefix='graphviz',

%s

\n''' % (fname, alt) self.body.append(svgtag) else: - with open(outfn + '.map', 'rb') as mapfile: - imgmap = mapfile.readlines() - if len(imgmap) == 2: - # nothing in image map (the lines are and ) - self.body.append('%s\n' % - (fname, alt, imgcss)) - else: - # has a map: get the name of the map and connect the parts - mapname = mapname_re.match(imgmap[0].decode('utf-8')).group(1) # type: ignore - self.body.append('%s\n' % - (fname, alt, mapname, imgcss)) - self.body.extend([item.decode('utf-8') for item in imgmap]) + with codecs.open(outfn + '.map', 'r', encoding='utf-8') as mapfile: # type: ignore + imgmap = ClickableMapDefinition(outfn + '.map', mapfile.read(), dot=code) + if imgmap.clickable: + # has a map + self.body.append('%s\n' % + (fname, alt, imgmap.id, imgcss)) + self.body.append(imgmap.generate_clickable_map()) + else: + # nothing in image map + self.body.append('%s\n' % + (fname, alt, imgcss)) if 'align' in node: self.body.append('\n') diff --git a/tests/test_ext_graphviz.py b/tests/test_ext_graphviz.py index 762add6f0..ef77135d7 100644 --- a/tests/test_ext_graphviz.py +++ b/tests/test_ext_graphviz.py @@ -13,6 +13,8 @@ import re import pytest +from sphinx.ext.graphviz import ClickableMapDefinition + @pytest.mark.sphinx('html', testroot='ext-graphviz') @pytest.mark.usefixtures('if_graphviz_found') @@ -115,3 +117,63 @@ def test_graphviz_i18n(app, status, warning): content = (app.outdir / 'index.html').text() html = 'digraph {\n  BAR -> BAZ\n}' assert re.search(html, content, re.M) + + +def test_graphviz_parse_mapfile(): + # empty graph + code = ('# digraph {\n' + '# }\n') + content = ('\n' + '') + cmap = ClickableMapDefinition('dummy.map', content, code) + assert cmap.filename == 'dummy.map' + assert cmap.id == 'grapvizb08107169e' + assert len(cmap.clickable) == 0 + assert cmap.generate_clickable_map() == '' + + # normal graph + code = ('digraph {\n' + ' foo [href="http://www.google.com/"];\n' + ' foo -> bar;\n' + '}\n') + content = ('\n' + '\n' + '') + cmap = ClickableMapDefinition('dummy.map', content, code) + assert cmap.filename == 'dummy.map' + assert cmap.id == 'grapviza4ccdd48ce' + assert len(cmap.clickable) == 1 + assert cmap.generate_clickable_map() == content.replace('%3', cmap.id) + + # inheritance-diagram:: sphinx.builders.html + content = ( + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '' + ) + cmap = ClickableMapDefinition('dummy.map', content, 'dummy_code') + assert cmap.filename == 'dummy.map' + assert cmap.id == 'inheritance66ff5471b9' + assert len(cmap.clickable) == 0 + assert cmap.generate_clickable_map() == ''