Fix #4501: graphviz: epub3 validation error caused if graph is not clickable

This commit is contained in:
Takeshi KOMIYA
2018-01-29 21:09:27 +09:00
parent dcbebb1286
commit 2312a0a5bc
3 changed files with 109 additions and 15 deletions

View File

@@ -24,6 +24,7 @@ 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
Testing
--------

View File

@@ -38,13 +38,47 @@ if False:
logger = logging.getLogger(__name__)
mapname_re = re.compile(r'<map id="(.*?)"')
class GraphvizError(SphinxError):
category = 'Graphviz error'
class ClickableMapDefinition(object):
"""A manipulator for clickable map file of graphviz."""
maptag_re = re.compile('<map id="(.*?)"')
href_re = re.compile('href=".*?"')
def __init__(self, filename, content):
# type: (unicode, unicode) -> None
self.id = None
self.filename = filename
self.content = content.splitlines()
self.clickable = [] # type: List[unicode]
self.parse()
def parse(self):
# type: () -> 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)
for line in self.content:
if self.href_re.search(line):
self.clickable.append(line)
def generate_clickable_map(self):
# type: () -> None
"""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
@@ -254,18 +288,17 @@ def render_dot_html(self, node, code, options, prefix='graphviz',
<p class="warning">%s</p></object>\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 <map> and </map>)
self.body.append('<img src="%s" alt="%s" %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('<img src="%s" alt="%s" usemap="#%s" %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:
imgmap = ClickableMapDefinition(outfn + '.map', mapfile.read())
if imgmap.clickable:
# has a map
self.body.append('<img src="%s" alt="%s" usemap="#%s" %s/>\n' %
(fname, alt, imgmap.id, imgcss))
self.body.append(imgmap.generate_clickable_map())
else:
# nothing in image map
self.body.append('<img src="%s" alt="%s" %s/>\n' %
(fname, alt, imgcss))
if 'align' in node:
self.body.append('</div>\n')

View File

@@ -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')
@@ -113,3 +115,61 @@ def test_graphviz_i18n(app, status, warning):
content = (app.outdir / 'index.html').text()
html = '<img src=".*?" alt="digraph {\n BAR -&gt; BAZ\n}" />'
assert re.search(html, content, re.M)
def test_graphviz_parse_mapfile():
# digraph {
# }
content = ('<map id="%3" name="%3">\n'
'</map>')
cmap = ClickableMapDefinition('dummy.map', content)
assert cmap.filename == 'dummy.map'
assert cmap.id == '%3'
assert len(cmap.clickable) == 0
assert cmap.generate_clickable_map() == ''
# digraph {
# foo [href="http://www.google.com/"];
# foo -> bar;
# }
content = ('<map id="%3" name="%3">\n'
'<area shape="poly" id="node1" href="http://www.google.com/" title="foo" alt=""'
' coords="77,29,76,22,70,15,62,10,52,7,41,5,30,7,20,10,12,15,7,22,5,29,7,37,12,'
'43,20,49,30,52,41,53,52,52,62,49,70,43,76,37"/>\n'
'</map>')
cmap = ClickableMapDefinition('dummy.map', content)
assert cmap.filename == 'dummy.map'
assert cmap.id == '%3'
assert len(cmap.clickable) == 1
assert cmap.generate_clickable_map() == content
# inheritance-diagram:: sphinx.builders.html
content = (
'<map id="inheritance66ff5471b9" name="inheritance66ff5471b9">\n'
'<area shape="rect" id="node1" title="Builds target formats from the reST sources."'
' alt="" coords="26,95,125,110"/>\n'
'<area shape="rect" id="node5" title="Builds standalone HTML docs."'
' alt="" coords="179,95,362,110"/>\n'
'<area shape="rect" id="node2" title="buildinfo file manipulator." '
' alt="" coords="14,64,138,80"/>\n'
'<area shape="rect" id="node3" title="The container of stylesheets."'
' alt="" coords="3,34,148,49"/>\n'
'<area shape="rect" id="node4" title="A StandaloneHTMLBuilder that creates all HTML'
' pages as &quot;index.html&quot; in" alt="" coords="395,64,569,80"/>\n'
'<area shape="rect" id="node7" title="An abstract builder that serializes'
' the generated HTML." alt="" coords="392,95,571,110"/>\n'
'<area shape="rect" id="node9" title="A StandaloneHTMLBuilder subclass that puts'
' the whole document tree on one" alt="" coords="393,125,570,141"/>\n'
'<area shape="rect" id="node6" title="A builder that dumps the generated HTML'
' into JSON files." alt="" coords="602,80,765,95"/>\n'
'<area shape="rect" id="node8" title="A Builder that dumps the generated HTML'
' into pickle files." alt="" coords="602,110,765,125"/>\n'
'<area shape="rect" id="node10" title="The metadata of stylesheet."'
' alt="" coords="11,3,141,19"/>\n'
'</map>'
)
cmap = ClickableMapDefinition('dummy.map', content)
assert cmap.filename == 'dummy.map'
assert cmap.id == 'inheritance66ff5471b9'
assert len(cmap.clickable) == 0
assert cmap.generate_clickable_map() == ''