diff options
-rw-r--r-- | CHANGES | 1 | ||||
-rw-r--r-- | doc/config.rst | 12 | ||||
-rw-r--r-- | sphinx/environment.py | 44 | ||||
-rw-r--r-- | sphinx/util/i18n.py | 25 | ||||
-rw-r--r-- | tests/roots/test-image-glob/rimg.xx.png | bin | 0 -> 218 bytes | |||
-rw-r--r-- | tests/roots/test-image-glob/subdir/index.rst | 2 | ||||
-rw-r--r-- | tests/roots/test-image-glob/subdir/rimg.png | bin | 0 -> 218 bytes | |||
-rw-r--r-- | tests/roots/test-image-glob/subdir/rimg.xx.png | bin | 0 -> 218 bytes | |||
-rw-r--r-- | tests/roots/test-image-glob/subdir/svgimg.xx.svg | 158 | ||||
-rw-r--r-- | tests/test_build.py | 18 | ||||
-rw-r--r-- | tests/test_intl.py | 47 | ||||
-rw-r--r-- | tests/test_util_i18n.py | 33 | ||||
-rw-r--r-- | tests/util.py | 10 |
13 files changed, 324 insertions, 26 deletions
@@ -106,6 +106,7 @@ Features added * #2320: classifier of glossary terms can be used for index entries grouping key. The classifier also be used for translation. See also :ref:`glossary-directive`. * Select an image by similarity if multiple images are globbed by ``.. image:: filename.*`` +* #1921: Support figure substitutions by :confval:`language` Bugs fixed ---------- diff --git a/doc/config.rst b/doc/config.rst index 9ac9b17d4..99ca15f02 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -417,12 +417,18 @@ documentation on :ref:`intl` for details. The code for the language the docs are written in. Any text automatically generated by Sphinx will be in that language. Also, Sphinx will try to substitute individual paragraphs from your documents with the translation - sets obtained from :confval:`locale_dirs`. In the LaTeX builder, a suitable - language will be selected as an option for the *Babel* package. Default is - ``None``, which means that no translation will be done. + sets obtained from :confval:`locale_dirs`. Sphinx will search + language-specific figures named by `figure_language_filename` and substitute + them for original figures. In the LaTeX builder, a suitable language will + be selected as an option for the *Babel* package. Default is ``None``, + which means that no translation will be done. .. versionadded:: 0.5 + .. versionchanged:: 1.4 + + Support figure substitution + Currently supported languages by Sphinx are: * ``bn`` -- Bengali diff --git a/sphinx/environment.py b/sphinx/environment.py index 42bfef931..b8b40b8e6 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -39,8 +39,9 @@ from sphinx.util import url_re, get_matching_docs, docname_join, split_into, \ FilenameUniqDict, split_index_msg from sphinx.util.nodes import clean_astext, make_refnode, WarningStream, is_translatable from sphinx.util.osutil import SEP, getcwd, fs_encoding, ensuredir -from sphinx.util.i18n import find_catalog_files from sphinx.util.images import guess_mimetype +from sphinx.util.i18n import find_catalog_files, get_image_filename_for_language, \ + search_image_for_language from sphinx.util.console import bold, purple from sphinx.util.matching import compile_matchers from sphinx.util.parallel import ParallelTasks, parallel_available, make_chunks @@ -884,6 +885,21 @@ class BuildEnvironment: def process_images(self, docname, doctree): """Process and rewrite image URIs.""" + def collect_candidates(imgpath, candidates): + globbed = {} + for filename in glob(imgpath): + new_imgpath = relative_path(path.join(self.srcdir, 'dummy'), + filename) + try: + mimetype = guess_mimetype(filename) + if mimetype not in candidates: + globbed.setdefault(mimetype, []).append(new_imgpath) + except (OSError, IOError) as err: + self.warn_node('image file %s not readable: %s' % + (filename, err), node) + for key, files in iteritems(globbed): + candidates[key] = sorted(files, key=len)[0] # select by similarity + for node in doctree.traverse(nodes.image): # Map the mimetype to the corresponding image. The writer may # choose the best image from these candidates. The special key * is @@ -896,21 +912,23 @@ class BuildEnvironment: candidates['?'] = imguri continue rel_imgpath, full_imgpath = self.relfn2path(imguri, docname) + if self.config.language: + # substitute figures (ex. foo.png -> foo.en.png) + i18n_full_imgpath = search_image_for_language(full_imgpath, self) + if i18n_full_imgpath != full_imgpath: + full_imgpath = i18n_full_imgpath + rel_imgpath = relative_path(path.join(self.srcdir, 'dummy'), + i18n_full_imgpath) # set imgpath as default URI node['uri'] = rel_imgpath if rel_imgpath.endswith(os.extsep + '*'): - globbed = {} - for filename in glob(full_imgpath): - new_imgpath = relative_path(path.join(self.srcdir, 'dummy'), - filename) - try: - mimetype = guess_mimetype(filename) - globbed.setdefault(mimetype, []).append(new_imgpath) - except (OSError, IOError) as err: - self.warn_node('image file %s not readable: %s' % - (filename, err), node) - for key, files in iteritems(globbed): - candidates[key] = sorted(files, key=len)[0] # select by similarity + if self.config.language: + # Search language-specific figures at first + i18n_imguri = get_image_filename_for_language(imguri, self) + _, full_i18n_imgpath = self.relfn2path(i18n_imguri, docname) + collect_candidates(full_i18n_imgpath, candidates) + + collect_candidates(full_imgpath, candidates) else: candidates['*'] = rel_imgpath diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 32ebee943..28170c385 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -22,6 +22,7 @@ import babel.dates from babel.messages.pofile import read_po from babel.messages.mofile import write_mo +from sphinx.errors import SphinxError from sphinx.util.osutil import walk from sphinx.util import SEP @@ -190,3 +191,27 @@ def format_date(format, date=None, language=None): result.append(token) return "".join(result) + + +def get_image_filename_for_language(filename, env): + if not env.config.language: + return filename + + root, ext = path.splitext(filename) + try: + return "{root}.{language}{ext}".format(root=root, ext=ext, + language=env.config.language) + except KeyError as exc: + raise SphinxError('Invalid figure_language_filename: %r' % exc) + + +def search_image_for_language(filename, env): + if not env.config.language: + return filename + + translated = get_image_filename_for_language(filename, env) + dirname = path.dirname(env.docname) + if path.exists(path.join(env.srcdir, dirname, translated)): + return translated + else: + return filename diff --git a/tests/roots/test-image-glob/rimg.xx.png b/tests/roots/test-image-glob/rimg.xx.png Binary files differnew file mode 100644 index 000000000..1081dc143 --- /dev/null +++ b/tests/roots/test-image-glob/rimg.xx.png diff --git a/tests/roots/test-image-glob/subdir/index.rst b/tests/roots/test-image-glob/subdir/index.rst index f086458d5..4ad2b0247 100644 --- a/tests/roots/test-image-glob/subdir/index.rst +++ b/tests/roots/test-image-glob/subdir/index.rst @@ -1,6 +1,8 @@ test-image-glob/subdir ====================== +.. image:: rimg.png + .. image:: svgimg.* .. figure:: svgimg.* diff --git a/tests/roots/test-image-glob/subdir/rimg.png b/tests/roots/test-image-glob/subdir/rimg.png Binary files differnew file mode 100644 index 000000000..1081dc143 --- /dev/null +++ b/tests/roots/test-image-glob/subdir/rimg.png diff --git a/tests/roots/test-image-glob/subdir/rimg.xx.png b/tests/roots/test-image-glob/subdir/rimg.xx.png Binary files differnew file mode 100644 index 000000000..1081dc143 --- /dev/null +++ b/tests/roots/test-image-glob/subdir/rimg.xx.png diff --git a/tests/roots/test-image-glob/subdir/svgimg.xx.svg b/tests/roots/test-image-glob/subdir/svgimg.xx.svg new file mode 100644 index 000000000..10e035b6d --- /dev/null +++ b/tests/roots/test-image-glob/subdir/svgimg.xx.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + height="60" + width="60" + _SVGFile__filename="oldscale/apps/warning.svg" + version="1.0" + y="0" + x="0" + id="svg1" + sodipodi:version="0.32" + inkscape:version="0.41" + sodipodi:docname="exclamation.svg" + sodipodi:docbase="/home/danny/work/icons/primary/scalable/actions"> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0000000" + inkscape:pageshadow="2" + inkscape:zoom="7.5136000" + inkscape:cx="42.825186" + inkscape:cy="24.316071" + inkscape:window-width="1020" + inkscape:window-height="691" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:current-layer="svg1" /> + <defs + id="defs3"> + <linearGradient + id="linearGradient1160"> + <stop + style="stop-color: #000000;stop-opacity: 1.0;" + id="stop1161" + offset="0" /> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + id="stop1162" + offset="1" /> + </linearGradient> + <linearGradient + xlink:href="#linearGradient1160" + id="linearGradient1163" /> + </defs> + <metadata + id="metadata12"> + <RDF + id="RDF13"> + <Work + about="" + id="Work14"> + <title + id="title15">Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004)</title> + <description + id="description17" /> + <subject + id="subject18"> + <Bag + id="Bag19"> + <li + id="li20" /> + </Bag> + </subject> + <publisher + id="publisher21"> + <Agent + about="" + id="Agent22"> + <title + id="title23" /> + </Agent> + </publisher> + <creator + id="creator24"> + <Agent + about="" + id="Agent25"> + <title + id="title26">Danny Allen</title> + </Agent> + </creator> + <rights + id="rights28"> + <Agent + about="" + id="Agent29"> + <title + id="title30">Danny Allen</title> + </Agent> + </rights> + <date + id="date32" /> + <format + id="format33">image/svg+xml</format> + <type + id="type35" + resource="http://purl.org/dc/dcmitype/StillImage" /> + <license + id="license36" + resource="http://creativecommons.org/licenses/LGPL/2.1/"> + <date + id="date37" /> + </license> + <language + id="language38">en</language> + </Work> + </RDF> + <rdf:RDF + id="RDF40"> + <cc:Work + rdf:about="" + id="Work41"> + <dc:format + id="format42">image/svg+xml</dc:format> + <dc:type + id="type44" + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + id="g2099"> + <path + style="color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:8.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none" + d="M 55.311891,51.920745 L 4.6880989,51.920744 L 29.999995,8.0792542 L 55.311891,51.920745 z " + id="path1724" /> + <path + style="color:#000000;fill:#ffe940;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#000000;stroke-width:3.1250010;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none" + d="M 55.311891,51.920745 L 4.6880989,51.920744 L 29.999995,8.0792542 L 55.311891,51.920745 z " + id="path1722" /> + <path + style="font-size:12.000000;font-weight:900;fill:none;fill-opacity:1.0000000;stroke:#ffffff;stroke-width:8.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000" + d="M 34.944960,10.779626 L 34.944960,33.186510 C 34.944960,34.752415 34.501979,36.081368 33.616007,37.173380 C 32.750636,38.265402 31.545298,38.811408 29.999995,38.811408 C 28.475302,38.811408 27.269965,38.265402 26.383993,37.173380 C 25.498020,36.060767 25.055030,34.731804 25.055030,33.186510 L 25.055030,10.779626 C 25.055030,9.1931155 25.498020,7.8641562 26.383993,6.7927462 C 27.269965,5.7007332 28.475302,5.1547262 29.999995,5.1547262 C 31.009593,5.1547262 31.885265,5.4019740 32.627010,5.8964706 C 33.389356,6.3909681 33.966274,7.0709005 34.357752,7.9362696 C 34.749221,8.7810349 34.944960,9.7288200 34.944960,10.779626 z " + id="path1099" /> + <path + style="font-size:12.000000;font-weight:900;fill:#e71c02;fill-opacity:1.0000000;stroke:none;stroke-width:3.1249981;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0000000" + d="M 29.999995,3.5986440 C 28.102272,3.5986440 26.318514,4.3848272 25.156245,5.8173940 C 24.028906,7.1806889 23.499995,8.9087770 23.499995,10.786144 L 23.499995,33.192394 C 23.499995,35.036302 24.050685,36.772771 25.156245,38.161144 C 26.318514,39.593721 28.102273,40.379893 29.999995,40.379894 C 31.913354,40.379894 33.697195,39.576736 34.843745,38.129894 C 35.959941,36.754118 36.499995,35.052976 36.499995,33.192394 L 36.499995,10.786144 C 36.499995,9.5413010 36.276626,8.3551469 35.781245,7.2861440 C 35.278844,6.1755772 34.477762,5.2531440 33.468745,4.5986440 C 32.454761,3.9226545 31.264694,3.5986439 29.999995,3.5986440 z " + id="path835" + sodipodi:nodetypes="cccccccccccc" /> + <path + style="color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:5.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none" + d="M 36.506243,49.901522 C 36.506243,53.492972 33.591442,56.407773 29.999991,56.407773 C 26.408541,56.407773 23.493739,53.492972 23.493739,49.901522 C 23.493739,46.310071 26.408541,43.395270 29.999991,43.395270 C 33.591442,43.395270 36.506243,46.310071 36.506243,49.901522 z " + id="path1727" /> + <path + style="color:#000000;fill:#e71c02;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:3.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none" + d="M 36.506243,49.901522 C 36.506243,53.492972 33.591442,56.407773 29.999991,56.407773 C 26.408541,56.407773 23.493739,53.492972 23.493739,49.901522 C 23.493739,46.310071 26.408541,43.395270 29.999991,43.395270 C 33.591442,43.395270 36.506243,46.310071 36.506243,49.901522 z " + id="path1725" /> + </g> +</svg> diff --git a/tests/test_build.py b/tests/test_build.py index ee6534b7d..1c0d55e1b 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -112,7 +112,7 @@ def test_numbered_circular_toctree(app, status, warning): 'contents <- sub <- contents') in warnings -@with_app(buildername='html', testroot='image-glob') +@with_app(buildername='dummy', testroot='image-glob') def test_image_glob(app, status, warning): app.builder.build_all() @@ -145,12 +145,16 @@ def test_image_glob(app, status, warning): doctree = pickle.loads((app.doctreedir / 'subdir/index.doctree').bytes()) assert isinstance(doctree[0][1], nodes.image) - assert doctree[0][1]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', + assert doctree[0][1]['candidates'] == {'*': 'subdir/rimg.png'} + assert doctree[0][1]['uri'] == 'subdir/rimg.png' + + assert isinstance(doctree[0][2], nodes.image) + assert doctree[0][2]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', 'image/svg+xml': 'subdir/svgimg.svg'} - assert doctree[0][1]['uri'] == 'subdir/svgimg.*' + assert doctree[0][2]['uri'] == 'subdir/svgimg.*' - assert isinstance(doctree[0][2], nodes.figure) - assert isinstance(doctree[0][2][0], nodes.image) - assert doctree[0][2][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', + assert isinstance(doctree[0][3], nodes.figure) + assert isinstance(doctree[0][3][0], nodes.image) + assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', 'image/svg+xml': 'subdir/svgimg.svg'} - assert doctree[0][2][0]['uri'] == 'subdir/svgimg.*' + assert doctree[0][3][0]['uri'] == 'subdir/svgimg.*' diff --git a/tests/test_intl.py b/tests/test_intl.py index b24ec65d2..61079b20e 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -13,6 +13,8 @@ from __future__ import print_function import os import re +import pickle +from docutils import nodes from subprocess import Popen, PIPE from xml.etree import ElementTree @@ -20,9 +22,9 @@ from babel.messages import pofile from nose.tools import assert_equal from six import string_types -from util import tempdir, rootdir, path, gen_with_app, SkipTest, \ +from util import tempdir, rootdir, path, gen_with_app, with_app, SkipTest, \ assert_re_search, assert_not_re_search, assert_in, assert_not_in, \ - assert_startswith + assert_startswith, assert_node root = tempdir / 'test-intl' @@ -794,3 +796,44 @@ def test_references(app, status, warning): warnings = warning.getvalue().replace(os.sep, '/') warning_expr = u'refs.txt:\\d+: ERROR: Unknown target name:' yield assert_count(warning_expr, warnings, 0) + + +@with_app(buildername='dummy', testroot='image-glob', confoverrides={'language': 'xx'}) +def test_image_glob_intl(app, status, warning): + app.builder.build_all() + + # index.rst + doctree = pickle.loads((app.doctreedir / 'index.doctree').bytes()) + + assert_node(doctree[0][1], nodes.image, uri='rimg.xx.png', + candidates={'*': 'rimg.xx.png'}) + + assert isinstance(doctree[0][2], nodes.figure) + assert_node(doctree[0][2][0], nodes.image, uri='rimg.xx.png', + candidates={'*': 'rimg.xx.png'}) + + assert_node(doctree[0][3], nodes.image, uri='img.*', + candidates={'application/pdf': 'img.pdf', + 'image/gif': 'img.gif', + 'image/png': 'img.png'}) + + assert isinstance(doctree[0][4], nodes.figure) + assert_node(doctree[0][4][0], nodes.image, uri='img.*', + candidates={'application/pdf': 'img.pdf', + 'image/gif': 'img.gif', + 'image/png': 'img.png'}) + + # subdir/index.rst + doctree = pickle.loads((app.doctreedir / 'subdir/index.doctree').bytes()) + + assert_node(doctree[0][1], nodes.image, uri='subdir/rimg.xx.png', + candidates={'*': 'subdir/rimg.xx.png'}) + + assert_node(doctree[0][2], nodes.image, uri='subdir/svgimg.*', + candidates={'application/pdf': 'subdir/svgimg.pdf', + 'image/svg+xml': 'subdir/svgimg.xx.svg'}) + + assert isinstance(doctree[0][3], nodes.figure) + assert_node(doctree[0][3][0], nodes.image, uri='subdir/svgimg.*', + candidates={'application/pdf': 'subdir/svgimg.pdf', + 'image/svg+xml': 'subdir/svgimg.xx.svg'}) diff --git a/tests/test_util_i18n.py b/tests/test_util_i18n.py index de7cf2ca7..01eb48c38 100644 --- a/tests/test_util_i18n.py +++ b/tests/test_util_i18n.py @@ -16,8 +16,9 @@ from os import path from babel.messages.mofile import read_mo from sphinx.util import i18n +from sphinx.errors import SphinxError -from util import with_tempdir +from util import TestApp, with_tempdir, raises def test_catalog_info_for_file_and_path(): @@ -183,3 +184,33 @@ def test_format_date(): assert i18n.format_date(format, date=date, language='en') == 'February 07, 2016' assert i18n.format_date(format, date=date, language='ja') == u'2月 07, 2016' assert i18n.format_date(format, date=date, language='de') == 'Februar 07, 2016' + + +def test_get_filename_for_language(): + app = TestApp() + + # language is None + app.env.config.language = None + assert app.env.config.language is None + assert i18n.get_image_filename_for_language('foo.png', app.env) == 'foo.png' + assert i18n.get_image_filename_for_language('foo.bar.png', app.env) == 'foo.bar.png' + assert i18n.get_image_filename_for_language('subdir/foo.png', app.env) == 'subdir/foo.png' + assert i18n.get_image_filename_for_language('../foo.png', app.env) == '../foo.png' + assert i18n.get_image_filename_for_language('foo', app.env) == 'foo' + + # language is en + app.env.config.language = 'en' + assert i18n.get_image_filename_for_language('foo.png', app.env) == 'foo.en.png' + assert i18n.get_image_filename_for_language('foo.bar.png', app.env) == 'foo.bar.en.png' + assert i18n.get_image_filename_for_language('dir/foo.png', app.env) == 'dir/foo.en.png' + assert i18n.get_image_filename_for_language('../foo.png', app.env) == '../foo.en.png' + assert i18n.get_image_filename_for_language('foo', app.env) == 'foo.en' + + # modify figure_language_filename and language is None + app.env.config.language = None + app.env.config.figure_language_filename = 'images/{language}/{root}{ext}' + assert i18n.get_image_filename_for_language('foo.png', app.env) == 'foo.png' + assert i18n.get_image_filename_for_language('foo.bar.png', app.env) == 'foo.bar.png' + assert i18n.get_image_filename_for_language('subdir/foo.png', app.env) == 'subdir/foo.png' + assert i18n.get_image_filename_for_language('../foo.png', app.env) == '../foo.png' + assert i18n.get_image_filename_for_language('foo', app.env) == 'foo' diff --git a/tests/util.py b/tests/util.py index 1e20e73e2..969c4e5c4 100644 --- a/tests/util.py +++ b/tests/util.py @@ -94,6 +94,16 @@ def assert_startswith(thing, prefix): assert False, '%r does not start with %r' % (thing, prefix) +def assert_node(node, cls=None, **kwargs): + if cls: + assert isinstance(node, cls), '%r is not subclass of %r' % (node, cls) + + for key, value in kwargs.items(): + assert key in node, '%r does not have %r attribute' % (node, key) + assert node[key] == value, \ + '%r[%s]: %r does not equals %r' % (node, key, node[key], value) + + try: from nose.tools import assert_in, assert_not_in except ImportError: |