Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit db0b215

Browse files
committed
Fix alt and caption handling in Sphinx directives
We currently template new reST to be re-parsed after the plot is created, but incorrectly copied the `alt` and `caption` values when they were wrapped. Additionally, change `figmpl` to use Sphinx/docutils' tag creation functions. These functions correctly escape attributes and so fixes invalid HTML when alt text contains quotes.
1 parent 0b7a88a commit db0b215

File tree

4 files changed

+53
-44
lines changed

4 files changed

+53
-44
lines changed

lib/matplotlib/sphinxext/figmpl_directive.py

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@
1212
See the *FigureMpl* documentation below.
1313
1414
"""
15-
from docutils import nodes
16-
17-
from docutils.parsers.rst import directives
18-
from docutils.parsers.rst.directives.images import Figure, Image
19-
2015
import os
2116
from os.path import relpath
2217
from pathlib import PurePath, Path
2318
import shutil
2419

20+
from docutils import nodes
21+
from docutils.parsers.rst import directives
22+
from docutils.parsers.rst.directives.images import Figure, Image
2523
from sphinx.errors import ExtensionError
2624

2725
import matplotlib
@@ -193,12 +191,13 @@ def visit_figmpl_html(self, node):
193191
# make uri also be relative...
194192
nm = PurePath(node['uri'][1:]).name
195193
uri = f'{imagerel}/{rel}{nm}'
194+
img_attrs = {'src': uri, 'alt': node['alt']}
196195

197196
# make srcset str. Need to change all the prefixes!
198197
maxsrc = uri
199-
srcsetst = ''
200198
if srcset:
201199
maxmult = -1
200+
srcsetst = ''
202201
for mult, src in srcset.items():
203202
nm = PurePath(src[1:]).name
204203
# ../../_images/plot_1_2_0x.png
@@ -214,44 +213,43 @@ def visit_figmpl_html(self, node):
214213
maxsrc = path
215214

216215
# trim trailing comma and space...
217-
srcsetst = srcsetst[:-2]
216+
img_attrs['srcset'] = srcsetst[:-2]
218217

219-
alt = node['alt']
220218
if node['class'] is not None:
221-
classst = ' '.join(node['class'])
222-
classst = f'class="{classst}"'
223-
224-
else:
225-
classst = ''
226-
227-
stylers = ['width', 'height', 'scale']
228-
stylest = ''
229-
for style in stylers:
219+
img_attrs['class'] = ' '.join(node['class'])
220+
for style in ['width', 'height', 'scale']:
230221
if node[style]:
231-
stylest += f'{style}: {node[style]};'
232-
233-
figalign = node['align'] if node['align'] else 'center'
234-
235-
# <figure class="align-default" id="id1">
236-
# <a class="reference internal image-reference" href="_images/index-1.2x.png">
237-
# <img alt="_images/index-1.2x.png" src="_images/index-1.2x.png" style="width: 53%;" />
238-
# </a>
239-
# <figcaption>
240-
# <p><span class="caption-text">Figure caption is here....</span>
241-
# <a class="headerlink" href="#id1" title="Permalink to this image">#</a></p>
242-
# </figcaption>
243-
# </figure>
244-
img_block = (f'<img src="{uri}" style="{stylest}" srcset="{srcsetst}" '
245-
f'alt="{alt}" {classst}/>')
246-
html_block = f'<figure class="align-{figalign}">\n'
247-
html_block += f' <a class="reference internal image-reference" href="{maxsrc}">\n'
248-
html_block += f' {img_block}\n </a>\n'
222+
if 'style' not in img_attrs:
223+
img_attrs['style'] = f'{style}: {node[style]};'
224+
else:
225+
img_attrs['style'] += f'{style}: {node[style]};'
226+
227+
# <figure class="align-default" id="id1">
228+
# <a class="reference internal image-reference" href="_images/index-1.2x.png">
229+
# <img alt="_images/index-1.2x.png"
230+
# src="_images/index-1.2x.png" style="width: 53%;" />
231+
# </a>
232+
# <figcaption>
233+
# <p><span class="caption-text">Figure caption is here....</span>
234+
# <a class="headerlink" href="#id1" title="Permalink to this image">#</a></p>
235+
# </figcaption>
236+
# </figure>
237+
self.body.append(
238+
self.starttag(
239+
node, 'figure',
240+
CLASS=f'align-{node["align"]}' if node['align'] else 'align-center'))
241+
self.body.append(
242+
self.starttag(node, 'a', CLASS='reference internal image-reference',
243+
href=maxsrc) +
244+
self.emptytag(node, 'img', **img_attrs) +
245+
'</a>\n')
249246
if node['caption']:
250-
html_block += ' <figcaption>\n'
251-
html_block += f' <p><span class="caption-text">{node["caption"]}</span></p>\n'
252-
html_block += ' </figcaption>\n'
253-
html_block += '</figure>\n'
254-
self.body.append(html_block)
247+
self.body.append(self.starttag(node, 'figcaption'))
248+
self.body.append(self.starttag(node, 'p'))
249+
self.body.append(self.starttag(node, 'span', CLASS='caption-text'))
250+
self.body.append(node['caption'])
251+
self.body.append('</span></p></figcaption>\n')
252+
self.body.append('</figure>\n')
255253

256254

257255
def visit_figmpl_latex(self, node):

lib/matplotlib/sphinxext/plot_directive.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ def run(arguments, content, options, state_machine, state, lineno):
876876

877877
# Properly indent the caption
878878
if caption and config.plot_srcset:
879-
caption = f':caption: {caption}'
879+
caption = f':caption: {caption.replace("\n", " ")}'
880880
elif caption:
881881
caption = '\n' + '\n'.join(' ' + line.strip()
882882
for line in caption.split('\n'))
@@ -896,6 +896,9 @@ def run(arguments, content, options, state_machine, state, lineno):
896896
if nofigs:
897897
images = []
898898

899+
if 'alt' in options:
900+
options['alt'] = options['alt'].replace('\n', ' ')
901+
899902
opts = [
900903
f':{key}: {val}' for key, val in options.items()
901904
if key in ('alt', 'height', 'width', 'scale', 'align', 'class')]

lib/matplotlib/tests/test_sphinxext.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,11 @@ def plot_directive_file(num):
8383
assert filecmp.cmp(range_6, img_dir / 'range6_range6.png')
8484
# check if figure caption made it into html file
8585
assert b'This is the caption for plot 15.' in html_contents
86-
# check if figure caption using :caption: made it into html file
87-
assert b'Plot 17 uses the caption option.' in html_contents
86+
# check if figure caption using :caption: made it into html file (because this plot
87+
# doesn't use srcset, the caption preserves newlines in the output.)
88+
assert b'Plot 17 uses the caption option,\nwith multi-line input.' in html_contents
89+
# check if figure alt text using :alt: made it into html file
90+
assert b'Plot 17 uses the alt option, with multi-line input.' in html_contents
8891
# check if figure caption made it into html file
8992
assert b'This is the caption for plot 18.' in html_contents
9093
# check if the custom classes made it into the html file

lib/matplotlib/tests/tinypages/some_plots.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,12 @@ Plot 16 uses a specific function in a file with plot commands:
135135
Plot 17 gets a caption specified by the :caption: option:
136136

137137
.. plot::
138-
:caption: Plot 17 uses the caption option.
138+
:caption:
139+
Plot 17 uses the caption option,
140+
with multi-line input.
141+
:alt:
142+
Plot 17 uses the alt option,
143+
with multi-line input.
139144

140145
plt.figure()
141146
plt.plot(range(6))

0 commit comments

Comments
 (0)