From 1f5aa28db085779684dc7770b738c70093c9cfd8 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 27 Jan 2016 01:36:43 +0900 Subject: [PATCH] Fix #1921: Support figure substitutions by locale --- CHANGES | 1 + doc/config.rst | 12 +- sphinx/environment.py | 44 +++-- sphinx/util/i18n.py | 25 +++ tests/roots/test-image-glob/rimg.xx.png | Bin 0 -> 218 bytes tests/roots/test-image-glob/subdir/index.rst | 2 + tests/roots/test-image-glob/subdir/rimg.png | Bin 0 -> 218 bytes .../roots/test-image-glob/subdir/rimg.xx.png | Bin 0 -> 218 bytes .../test-image-glob/subdir/svgimg.xx.svg | 158 ++++++++++++++++++ tests/test_build.py | 20 ++- tests/test_intl.py | 47 +++++- tests/test_util_i18n.py | 33 +++- tests/util.py | 10 ++ 13 files changed, 325 insertions(+), 27 deletions(-) create mode 100644 tests/roots/test-image-glob/rimg.xx.png create mode 100644 tests/roots/test-image-glob/subdir/rimg.png create mode 100644 tests/roots/test-image-glob/subdir/rimg.xx.png create mode 100644 tests/roots/test-image-glob/subdir/svgimg.xx.svg diff --git a/CHANGES b/CHANGES index ee0234561..117a7bc63 100644 --- a/CHANGES +++ b/CHANGES @@ -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 new file mode 100644 index 0000000000000000000000000000000000000000..1081dc1439fb984dfa7ef627afe3c7dc476fdbce GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^j6iI|!3HFkf4uMuBv2gW?!>U}oXkrghqJ&VvY3H^ zTNs2H8D`Cq01C2~c>21s-(chw7$R|bZ|_0D0|q>YSbqDzW^|HYIk%*-&O)*U}oXkrghqJ&VvY3H^ zTNs2H8D`Cq01C2~c>21s-(chw7$R|bZ|_0D0|q>YSbqDzW^|HYIk%*-&O)*U}oXkrghqJ&VvY3H^ zTNs2H8D`Cq01C2~c>21s-(chw7$R|bZ|_0D0|q>YSbqDzW^|HYIk%*-&O)* + + + + + + + + + + + + + Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004) + + + +
  • + + + + + + </Agent> + </publisher> + <creator + id="creator24"> + <Agent + about="" + id="Agent25"> + <title + id="title26">Danny Allen + + + + + Danny Allen + + + + image/svg+xml + + + + + en + + + + + image/svg+xml + + + + + + + + + + + + + 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', - 'image/svg+xml': 'subdir/svgimg.svg'} - assert doctree[0][1]['uri'] == 'subdir/svgimg.*' + assert doctree[0][1]['candidates'] == {'*': 'subdir/rimg.png'} + assert doctree[0][1]['uri'] == 'subdir/rimg.png' - 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][2], nodes.image) + assert doctree[0][2]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', + 'image/svg+xml': 'subdir/svgimg.svg'} + assert doctree[0][2]['uri'] == 'subdir/svgimg.*' + + 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: