URI-escape image filenames (#10268)

Without this change, local images with `#` in their name result in incorrect URLs

There is already a similar call to `urllib.parse.quote` for file downloads, suggesting this is a sensible approach.

Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Co-authored-by: Takeshi KOMIYA <i.tkomiya@gmail.com>
This commit is contained in:
Eric Wieser 2022-10-13 17:37:07 +01:00 committed by GitHub
parent e008e16200
commit fa6d42597f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 28 additions and 9 deletions

View File

@ -20,6 +20,7 @@ Features added
``:option:`--module[=foobar]``` or ``:option:`--module foobar```. ``:option:`--module[=foobar]``` or ``:option:`--module foobar```.
Patch by Martin Liska. Patch by Martin Liska.
* #10881: autosectionlabel: Record the generated section label to the debug log. * #10881: autosectionlabel: Record the generated section label to the debug log.
* #10268: Correctly URI-escape image filenames.
Bugs fixed Bugs fixed
---------- ----------

View File

@ -5,6 +5,7 @@ import os
import re import re
from os import path from os import path
from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
from urllib.parse import quote
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
from docutils import nodes from docutils import nodes
@ -524,7 +525,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
type='epub', subtype='unknown_project_files') type='epub', subtype='unknown_project_files')
continue continue
filename = filename.replace(os.sep, '/') filename = filename.replace(os.sep, '/')
item = ManifestItem(html.escape(filename), item = ManifestItem(html.escape(quote(filename)),
html.escape(self.make_id(filename)), html.escape(self.make_id(filename)),
html.escape(self.media_types[ext])) html.escape(self.media_types[ext]))
metadata['manifest_items'].append(item) metadata['manifest_items'].append(item)

View File

@ -620,7 +620,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator):
# rewrite the URI if the environment knows about it # rewrite the URI if the environment knows about it
if olduri in self.builder.images: if olduri in self.builder.images:
node['uri'] = posixpath.join(self.builder.imgpath, node['uri'] = posixpath.join(self.builder.imgpath,
self.builder.images[olduri]) urllib.parse.quote(self.builder.images[olduri]))
if 'scale' in node: if 'scale' in node:
# Try to figure out image height and width. Docutils does that too, # Try to figure out image height and width. Docutils does that too,

View File

@ -567,7 +567,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
# rewrite the URI if the environment knows about it # rewrite the URI if the environment knows about it
if olduri in self.builder.images: if olduri in self.builder.images:
node['uri'] = posixpath.join(self.builder.imgpath, node['uri'] = posixpath.join(self.builder.imgpath,
self.builder.images[olduri]) urllib.parse.quote(self.builder.images[olduri]))
if 'scale' in node: if 'scale' in node:
# Try to figure out image height and width. Docutils does that too, # Try to figure out image height and width. Docutils does that too,

View File

@ -1319,14 +1319,17 @@ class LaTeXTranslator(SphinxTranslator):
if include_graphics_options: if include_graphics_options:
options = '[%s]' % ','.join(include_graphics_options) options = '[%s]' % ','.join(include_graphics_options)
base, ext = path.splitext(uri) base, ext = path.splitext(uri)
if self.in_title and base: if self.in_title and base:
# Lowercase tokens forcely because some fncychap themes capitalize # Lowercase tokens forcely because some fncychap themes capitalize
# the options of \sphinxincludegraphics unexpectedly (ex. WIDTH=...). # the options of \sphinxincludegraphics unexpectedly (ex. WIDTH=...).
self.body.append(r'\lowercase{\sphinxincludegraphics%s}{{%s}%s}' % cmd = r'\lowercase{\sphinxincludegraphics%s}{{%s}%s}' % (options, base, ext)
(options, base, ext))
else: else:
self.body.append(r'\sphinxincludegraphics%s{{%s}%s}' % cmd = r'\sphinxincludegraphics%s{{%s}%s}' % (options, base, ext)
(options, base, ext)) # escape filepath for includegraphics, https://tex.stackexchange.com/a/202714/41112
if '#' in base:
cmd = r'{\catcode`\#=12' + cmd + '}'
self.body.append(cmd)
self.body.extend(post) self.body.extend(post)
def depart_image(self, node: Element) -> None: def depart_image(self, node: Element) -> None:

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,5 @@
Sphinx image handling
=====================
.. an image with a character that is valid in a local file path but not a URL
.. image:: img_#1.png

View File

@ -1397,6 +1397,15 @@ def test_html_remote_images(app, status, warning):
assert not (app.outdir / 'python-logo.png').exists() assert not (app.outdir / 'python-logo.png').exists()
@pytest.mark.sphinx('html', testroot='image-escape')
def test_html_encoded_image(app, status, warning):
app.builder.build_all()
result = (app.outdir / 'index.html').read_text()
assert ('<img alt="_images/img_%231.png" src="_images/img_%231.png" />' in result)
assert (app.outdir / '_images/img_#1.png').exists()
@pytest.mark.sphinx('html', testroot='remote-logo') @pytest.mark.sphinx('html', testroot='remote-logo')
def test_html_remote_logo(app, status, warning): def test_html_remote_logo(app, status, warning):
app.builder.build_all() app.builder.build_all()

View File

@ -59,8 +59,8 @@ def compile_latex_document(app, filename='python.tex'):
except OSError as exc: # most likely the latex executable was not found except OSError as exc: # most likely the latex executable was not found
raise pytest.skip.Exception from exc raise pytest.skip.Exception from exc
except CalledProcessError as exc: except CalledProcessError as exc:
print(exc.stdout) print(exc.stdout.decode('utf8'))
print(exc.stderr) print(exc.stderr.decode('utf8'))
raise AssertionError('%s exited with return code %s' % (app.config.latex_engine, raise AssertionError('%s exited with return code %s' % (app.config.latex_engine,
exc.returncode)) exc.returncode))