diff options
30 files changed, 864 insertions, 53 deletions
@@ -20,6 +20,8 @@ Incompatible changes templates directory. * Custom domains should implement the new `Domain.resolve_any_xref` method to make the `any` role work properly. +* gettext builder: disable extracting/apply 'index' node by default. Please set + 'index' to :confval:`gettext_enables` to enable extracting index entries. Features added -------------- @@ -44,6 +46,8 @@ Features added directive is now supported. * PR#214: Added stemming support for 14 languages, so that the built-in document search can now handle these. Thanks to Shibukawa Yoshiki. +* PR#296: numfig feature: Assign numbers to figures, tables and code-blocks. + Thanks to Takeshi Komiya. * PR#202: Allow "." and "~" prefixed references in ``:param:`` doc fields for Python. * PR#184: Add `autodoc_mock_imports`, allowing to mock imports of @@ -83,6 +87,10 @@ Features added ``+``. * PR#291: The caption of :rst:dir:`code-block` is recognised as a title of ref target. Thanks to Takeshi Komiya. +* PR#298: Add new API: :meth:`~sphinx.application.Sphinx.add_latex_package`. + Thanks to Takeshi Komiya. +* #1344: add :confval:`gettext_enables` to enable extracting 'index' to gettext + catalog output / applying translation catalog to generated documentation. Bugs fixed ---------- @@ -147,6 +155,9 @@ Bugs fixed to avoid separation of caption and body. Thanks to Takeshi Komiya. * PR#295, #1520: ``make.bat latexpdf`` mechanism to ``cd`` back to the current directory. Thanks to Peter Suter. +* PR#297, #1571: Add imgpath property to all builders. It make easier to + develop builder extensions. Thanks to Takeshi Komiya. +* #1584: Point to master doc in HTML "top" link. Documentation ------------- diff --git a/doc/config.rst b/doc/config.rst index a11254ea..f181e5c5 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -447,6 +447,16 @@ documentation on :ref:`intl` for details. .. versionadded:: 1.3 +.. confval:: gettext_enables + + To specify names to enable gettext extracting and translation applying for + i18n. You can specify below names: + + :index: index terms + + The default is ``[]``. + + .. versionadded:: 1.3 .. _html-options: diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index 4fed158c..d0d49286 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -288,6 +288,18 @@ package. .. versionadded:: 1.0 +.. method:: Sphinx.add_latex_package(packagename, options=None) + + Add *packagename* to the list of packages that LaTeX source code will include. + If you provide *options*, it will be taken to `\usepackage` declaration. + + .. code-block:: python + + app.add_latex_package('mypackage') # => \usepackage{mypackage} + app.add_latex_package('mypackage', 'foo,bar') # => \usepackage[foo,bar]{mypackage} + + .. versionadded:: 1.3 + .. method:: Sphinx.add_lexer(alias, lexer) Use *lexer*, which must be an instance of a Pygments lexer class, to diff --git a/sphinx/application.py b/sphinx/application.py index 13a2d272..630bff36 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -694,6 +694,11 @@ class Sphinx(object): StandaloneHTMLBuilder.css_files.append( posixpath.join('_static', filename)) + def add_latex_package(self, packagename, options=None): + self.debug('[app] adding latex package: %r', packagename) + from sphinx.builders.latex import LaTeXBuilder + LaTeXBuilder.usepackages.append((packagename, options)) + def add_lexer(self, alias, lexer): self.debug('[app] adding lexer: %r', (alias, lexer)) from sphinx.highlighting import lexers diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index d52a7983..7d1bd920 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -70,6 +70,10 @@ class Builder(object): # images that need to be copied over (source -> dest) self.images = {} + # basename of images directory + self.imagedir = "" + # relative path to image directory from current docname (used at writing docs) + self.imgpath = "" # these get set later self.parallel_ok = False diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index 5f9f6643..848dfaa4 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -404,7 +404,7 @@ class EpubBuilder(StandaloneHTMLBuilder): The method tries to read and write the files with the PIL, converting the format and resizing the image if necessary/possible. """ - ensuredir(path.join(self.outdir, '_images')) + ensuredir(path.join(self.outdir, self.imagedir)) for src in self.app.status_iterator(self.images, 'copying images... ', brown, len(self.images)): dest = self.images[src] @@ -416,7 +416,7 @@ class EpubBuilder(StandaloneHTMLBuilder): (path.join(self.srcdir, src), )) try: copyfile(path.join(self.srcdir, src), - path.join(self.outdir, '_images', dest)) + path.join(self.outdir, self.imagedir, dest)) except (IOError, OSError) as err: self.warn('cannot copy image file %r: %s' % (path.join(self.srcdir, src), err)) @@ -432,7 +432,7 @@ class EpubBuilder(StandaloneHTMLBuilder): nh = (height * nw) / width img = img.resize((nw, nh), Image.BICUBIC) try: - img.save(path.join(self.outdir, '_images', dest)) + img.save(path.join(self.outdir, self.imagedir, dest)) except (IOError, OSError) as err: self.warn('cannot write image file %r: %s' % (path.join(self.srcdir, src), err)) diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index 082059fd..01fa06a6 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -108,15 +108,16 @@ class I18nBuilder(Builder): for node, msg in extract_messages(doctree): catalog.add(msg, node) - # Extract translatable messages from index entries. - for node, entries in traverse_translatable_index(doctree): - for typ, msg, tid, main in entries: - for m in split_index_msg(typ, msg): - if typ == 'pair' and m in pairindextypes.values(): - # avoid built-in translated message was incorporated - # in 'sphinx.util.nodes.process_index_entry' - continue - catalog.add(m, node) + if 'index' in self.env.config.gettext_enables: + # Extract translatable messages from index entries. + for node, entries in traverse_translatable_index(doctree): + for typ, msg, tid, main in entries: + for m in split_index_msg(typ, msg): + if typ == 'pair' and m in pairindextypes.values(): + # avoid built-in translated message was incorporated + # in 'sphinx.util.nodes.process_index_entry' + continue + catalog.add(m, node) # determine tzoffset once to remain unaffected by DST change during build diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index c2c30893..d33d2eef 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -95,6 +95,8 @@ class StandaloneHTMLBuilder(Builder): # a hash of all config values that, if changed, cause a full rebuild self.config_hash = '' self.tags_hash = '' + # basename of images directory + self.imagedir = '_images' # section numbers for headings in the currently visited document self.secnumbers = {} # currently written docname @@ -424,6 +426,7 @@ class StandaloneHTMLBuilder(Builder): doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) + self.fignumbers = self.env.toc_fignumbers.get(docname, {}) self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads') self.current_docname = docname @@ -436,7 +439,7 @@ class StandaloneHTMLBuilder(Builder): self.handle_page(docname, ctx, event_arg=doctree) def write_doc_serialized(self, docname, doctree): - self.imgpath = relative_uri(self.get_target_uri(docname), '_images') + self.imgpath = relative_uri(self.get_target_uri(docname), self.imagedir) self.post_process_images(doctree) title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' @@ -534,13 +537,13 @@ class StandaloneHTMLBuilder(Builder): def copy_image_files(self): # copy image files if self.images: - ensuredir(path.join(self.outdir, '_images')) + ensuredir(path.join(self.outdir, self.imagedir)) for src in self.app.status_iterator(self.images, 'copying images... ', brown, len(self.images)): dest = self.images[src] try: copyfile(path.join(self.srcdir, src), - path.join(self.outdir, '_images', dest)) + path.join(self.outdir, self.imagedir, dest)) except Exception as err: self.warn('cannot copy image file %r: %s' % (path.join(self.srcdir, src), err)) @@ -1028,6 +1031,7 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): def init(self): self.config_hash = '' self.tags_hash = '' + self.imagedir = '_images' self.theme = None # no theme necessary self.templates = None # no template bridge necessary self.init_translator_class() diff --git a/sphinx/builders/latex.py b/sphinx/builders/latex.py index bf7991cf..5683ade2 100644 --- a/sphinx/builders/latex.py +++ b/sphinx/builders/latex.py @@ -37,6 +37,7 @@ class LaTeXBuilder(Builder): format = 'latex' supported_image_types = ['application/pdf', 'image/png', 'image/gif', 'image/jpeg'] + usepackages = [] def init(self): self.docnames = [] diff --git a/sphinx/builders/qthelp.py b/sphinx/builders/qthelp.py index c0fff2a6..8e2558e7 100644 --- a/sphinx/builders/qthelp.py +++ b/sphinx/builders/qthelp.py @@ -157,7 +157,7 @@ class QtHelpBuilder(StandaloneHTMLBuilder): olen = len(outdir) projectfiles = [] staticdir = path.join(outdir, '_static') - imagesdir = path.join(outdir, '_images') + imagesdir = path.join(outdir, self.imagedir) for root, dirs, files in os.walk(outdir): resourcedir = root.startswith(staticdir) or \ root.startswith(imagesdir) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 7b0e6f72..619ef6fe 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -58,7 +58,8 @@ class WebSupportBuilder(PickleHTMLBuilder): doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) - self.imgpath = '/' + posixpath.join(self.virtual_staticdir, '_images') + self.fignumbers = self.env.toc_fignumbers.get(docname, {}) + self.imgpath = '/' + posixpath.join(self.virtual_staticdir, self.imagedir) self.dlpath = '/' + posixpath.join(self.virtual_staticdir, '_downloads') self.current_docname = docname self.docwriter.write(doctree, destination) @@ -70,7 +71,7 @@ class WebSupportBuilder(PickleHTMLBuilder): self.handle_page(docname, ctx, event_arg=doctree) def write_doc_serialized(self, docname, doctree): - self.imgpath = '/' + posixpath.join(self.virtual_staticdir, '_images') + self.imgpath = '/' + posixpath.join(self.virtual_staticdir, self.imagedir) self.post_process_images(doctree) title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' @@ -148,7 +149,7 @@ class WebSupportBuilder(PickleHTMLBuilder): PickleHTMLBuilder.handle_finish(self) # move static stuff over to separate directory - directories = ['_images', '_static'] + directories = [self.imagedir, '_static'] for directory in directories: src = path.join(self.outdir, directory) dst = path.join(self.staticdir, directory) diff --git a/sphinx/config.py b/sphinx/config.py index 40536ae5..8593a190 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -70,6 +70,12 @@ class Config(object): needs_extensions = ({}, None), nitpicky = (False, 'env'), nitpick_ignore = ([], 'html'), + numfig = (False, 'env'), + numfig_secnum_depth = (1, 'env'), + numfig_prefix = ({'figure': l_('Fig.'), + 'table': l_('Table '), + 'code-block': l_('List ')}, + 'env'), # HTML options html_theme = ('default', 'html'), @@ -205,6 +211,7 @@ class Config(object): gettext_location = (True, 'gettext'), gettext_uuid = (True, 'gettext'), gettext_auto_build = (True, 'env'), + gettext_enables = ([], 'env'), # XML options xml_pretty = (True, 'env'), diff --git a/sphinx/environment.py b/sphinx/environment.py index 1ce284f6..d9be0be5 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -48,7 +48,7 @@ from sphinx.errors import SphinxError, ExtensionError from sphinx.locale import _ from sphinx.versioning import add_uids, merge_doctrees from sphinx.transforms import DefaultSubstitutions, MoveModuleTargets, \ - HandleCodeBlocks, SortIds, CitationReferences, Locale, \ + HandleCodeBlocks, AutoNumbering, SortIds, CitationReferences, Locale, \ RemoveTranslatableInline, SphinxContentsFilter @@ -98,7 +98,7 @@ class SphinxStandaloneReader(standalone.Reader): Add our own transforms. """ transforms = [Locale, CitationReferences, DefaultSubstitutions, - MoveModuleTargets, HandleCodeBlocks, SortIds, + MoveModuleTargets, HandleCodeBlocks, AutoNumbering, SortIds, RemoveTranslatableInline] def get_transforms(self): @@ -234,6 +234,8 @@ class BuildEnvironment: # used to determine when to show the TOC # in a sidebar (don't show if it's only one item) self.toc_secnumbers = {} # docname -> dict of sectionid -> number + self.toc_fignumbers = {} # docname -> dict of figtype -> + # dict of figureid -> number self.toctree_includes = {} # docname -> list of toctree includefiles self.files_to_rebuild = {} # docname -> set of files @@ -635,8 +637,8 @@ class BuildEnvironment: self._warnfunc(*warning) def check_dependents(self, already): - to_rewrite = self.assign_section_numbers() - for docname in to_rewrite: + to_rewrite = self.assign_section_numbers() + self.assign_figure_numbers() + for docname in set(to_rewrite): if docname not in already: yield docname @@ -1693,6 +1695,87 @@ class BuildEnvironment: return rewrite_needed + def assign_figure_numbers(self): + """Assign a figure number to each figure under a numbered toctree.""" + + rewrite_needed = [] + + assigned = set() + old_fignumbers = getattr(self, 'toc_fignumbers', {}) # compatible with old envs + self.toc_fignumbers = {} + fignum_counter = {} + + def has_child(node, cls): + return any(isinstance(child, cls) for child in node) + + def get_section_number(docname, section): + anchorname = '#' + section['ids'][0] + secnumbers = self.toc_secnumbers.get(docname, {}) + if anchorname in secnumbers: + secnum = secnumbers.get(anchorname) + else: + secnum = secnumbers.get('') + + return secnum or tuple() + + def get_next_fignumber(figtype, secnum): + counter = fignum_counter.setdefault(figtype, {}) + + secnum = secnum[:self.config.numfig_secnum_depth] + counter[secnum] = counter.get(secnum, 0) + 1 + return secnum + (counter[secnum],) + + def register_fignumber(docname, secnum, figtype, figure_id): + self.toc_fignumbers.setdefault(docname, {}) + fignumbers = self.toc_fignumbers[docname].setdefault(figtype, {}) + fignumbers[figure_id] = get_next_fignumber(figtype, secnum) + + def _walk_doctree(docname, doctree, secnum): + for subnode in doctree.children: + if isinstance(subnode, nodes.section): + next_secnum = get_section_number(docname, subnode) + if next_secnum: + _walk_doctree(docname, subnode, next_secnum) + else: + _walk_doctree(docname, subnode, secnum) + continue + elif isinstance(subnode, addnodes.toctree): + for title, subdocname in subnode['entries']: + if url_re.match(subdocname) or subdocname == 'self': + # don't mess with those + continue + + _walk_doc(subdocname, secnum) + + continue + + if isinstance(subnode, nodes.figure): + figure_id = subnode['ids'][0] + register_fignumber(docname, secnum, 'figure', figure_id) + elif isinstance(subnode, nodes.table): + table_id = subnode['ids'][0] + register_fignumber(docname, secnum, 'table', table_id) + elif isinstance(subnode, nodes.container): + if has_child(subnode, nodes.literal_block): + code_block_id = subnode['ids'][0] + register_fignumber(docname, secnum, 'code-block', code_block_id) + + _walk_doctree(docname, subnode, secnum) + + def _walk_doc(docname, secnum): + if docname not in assigned: + assigned.add(docname) + doctree = self.get_doctree(docname) + _walk_doctree(docname, doctree, secnum) + + if self.config.numfig: + _walk_doc(self.config.master_doc, tuple()) + for docname, fignums in iteritems(self.toc_fignumbers): + if fignums != old_fignumbers.get(docname): + rewrite_needed.append(docname) + + return rewrite_needed + def create_index(self, builder, group_entries=True, _fixre=re.compile(r'(.*) ([(][^()]*[)])')): """Create the real index from the collected index entries.""" diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 56831c64..71e7ba65 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -146,14 +146,8 @@ def render_dot(self, code, options, format, prefix='graphviz'): ).encode('utf-8') fname = '%s-%s.%s' % (prefix, sha1(hashkey).hexdigest(), format) - if hasattr(self.builder, 'imgpath'): - # HTML - relfn = posixpath.join(self.builder.imgpath, fname) - outfn = path.join(self.builder.outdir, '_images', fname) - else: - # LaTeX - relfn = fname - outfn = path.join(self.builder.outdir, fname) + relfn = posixpath.join(self.builder.imgpath, fname) + outfn = path.join(self.builder.outdir, self.builder.imagedir, fname) if path.isfile(outfn): return relfn, outfn diff --git a/sphinx/ext/pngmath.py b/sphinx/ext/pngmath.py index 27d7978f..81a6f9d5 100644 --- a/sphinx/ext/pngmath.py +++ b/sphinx/ext/pngmath.py @@ -86,7 +86,7 @@ def render_math(self, math): shasum = "%s.png" % sha1(latex.encode('utf-8')).hexdigest() relfn = posixpath.join(self.builder.imgpath, 'math', shasum) - outfn = path.join(self.builder.outdir, '_images', 'math', shasum) + outfn = path.join(self.builder.outdir, self.builder.imagedir, 'math', shasum) if path.isfile(outfn): depth = read_png_depth(outfn) return relfn, depth diff --git a/sphinx/themes/basic/layout.html b/sphinx/themes/basic/layout.html index f33b7cbe..a7bbbb9a 100644 --- a/sphinx/themes/basic/layout.html +++ b/sphinx/themes/basic/layout.html @@ -139,7 +139,7 @@ {%- if hasdoc('copyright') %} <link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" /> {%- endif %} - <link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}" /> + <link rel="top" title="{{ docstitle|e }}" href="{{ pathto(master_doc) }}" /> {%- if parents %} <link rel="up" title="{{ parents[-1].title|striptags|e }}" href="{{ parents[-1].link|e }}" /> {%- endif %} diff --git a/sphinx/themes/bizstyle/static/bizstyle.css_t b/sphinx/themes/bizstyle/static/bizstyle.css_t index 80b6dfe3..2b3964c2 100644 --- a/sphinx/themes/bizstyle/static/bizstyle.css_t +++ b/sphinx/themes/bizstyle/static/bizstyle.css_t @@ -244,13 +244,13 @@ cite, code, tt { letter-spacing: 0.01em; } -tt { +code { background-color: #F2F2F2; border-bottom: 1px solid #ddd; color: #333; } -tt.descname, tt.descclassname, tt.xref { +code.descname, code.descclassname, code.xref { border: 0; } @@ -259,12 +259,12 @@ hr { margin: 2em; } -a tt { +a code { border: 0; color: #CA7900; } -a tt:hover { +a code:hover { color: #2491CF; } diff --git a/sphinx/transforms.py b/sphinx/transforms.py index 42abea58..a62fa99a 100644 --- a/sphinx/transforms.py +++ b/sphinx/transforms.py @@ -101,6 +101,31 @@ class HandleCodeBlocks(Transform): # del node.parent[parindex+1] +class AutoNumbering(Transform): + """ + Register IDs of tables, figures and literal_blocks to assign numbers. + """ + default_priority = 210 + + def apply(self): + def has_child(node, cls): + return any(isinstance(child, cls) for child in node) + + for node in self.document.traverse(nodes.Element): + if isinstance(node, nodes.figure): + if has_child(node, nodes.caption): + self.document.note_implicit_target(node) + elif isinstance(node, nodes.image): + if has_child(node.parent, nodes.caption): + self.document.note_implicit_target(node.parent) + elif isinstance(node, nodes.table): + if has_child(node, nodes.title): + self.document.note_implicit_target(node) + elif isinstance(node, nodes.literal_block): + if has_child(node.parent, nodes.caption): + self.document.note_implicit_target(node.parent) + + class SortIds(Transform): """ Sort secion IDs so that the "id[0-9]+" one comes last. @@ -437,22 +462,23 @@ class Locale(Transform): node.children = patch.children node['translated'] = True - # Extract and translate messages for index entries. - for node, entries in traverse_translatable_index(self.document): - new_entries = [] - for type, msg, tid, main in entries: - msg_parts = split_index_msg(type, msg) - msgstr_parts = [] - for part in msg_parts: - msgstr = catalog.gettext(part) - if not msgstr: - msgstr = part - msgstr_parts.append(msgstr) - - new_entries.append((type, ';'.join(msgstr_parts), tid, main)) - - node['raw_entries'] = entries - node['entries'] = new_entries + if 'index' in env.config.gettext_enables: + # Extract and translate messages for index entries. + for node, entries in traverse_translatable_index(self.document): + new_entries = [] + for type, msg, tid, main in entries: + msg_parts = split_index_msg(type, msg) + msgstr_parts = [] + for part in msg_parts: + msgstr = catalog.gettext(part) + if not msgstr: + msgstr = part + msgstr_parts.append(msgstr) + + new_entries.append((type, ';'.join(msgstr_parts), tid, main)) + + node['raw_entries'] = entries + node['entries'] = new_entries class RemoveTranslatableInline(Transform): diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 56657ee7..82c228bd 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -250,6 +250,20 @@ class HTMLTranslator(BaseTranslator): self.body.append('.'.join(map(str, numbers)) + self.secnumber_suffix) + def add_fignumber(self, node): + def append_fignumber(figtype, figure_id): + if figure_id in self.builder.fignumbers.get(figtype, {}): + prefix = self.builder.config.numfig_prefix.get(figtype, '') + numbers = self.builder.fignumbers[figtype][figure_id] + self.body.append(prefix + '.'.join(map(str, numbers)) + " ") + + if isinstance(node.parent, nodes.figure): + append_fignumber('figure', node.parent['ids'][0]) + elif isinstance(node.parent, nodes.table): + append_fignumber('table', node.parent['ids'][0]) + elif isinstance(node.parent, nodes.container): + append_fignumber('code-block', node.parent['ids'][0]) + # overwritten to avoid emitting empty <ul></ul> def visit_bullet_list(self, node): if len(node) == 1 and node[0].tagname == 'toctree': @@ -260,6 +274,7 @@ class HTMLTranslator(BaseTranslator): def visit_title(self, node): BaseTranslator.visit_title(self, node) self.add_secnumber(node) + self.add_fignumber(node) # overwritten def visit_literal_block(self, node): @@ -291,6 +306,7 @@ class HTMLTranslator(BaseTranslator): self.body.append(self.starttag(node, 'div', '', CLASS='code-block-caption')) else: BaseTranslator.visit_caption(self, node) + self.add_fignumber(node) def depart_caption(self, node): if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index abc7ed75..ea79761f 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -42,6 +42,7 @@ HEADER = r'''%% Generated by Sphinx. %(longtable)s \usepackage{sphinx} \usepackage{multirow} +%(usepackages)s %(preamble)s \title{%(title)s} @@ -153,6 +154,7 @@ class LaTeXTranslator(nodes.NodeVisitor): 'fontpkg': '\\usepackage{times}', 'fncychap': '\\usepackage[Bjarne]{fncychap}', 'longtable': '\\usepackage{longtable}', + 'usepackages': '', 'preamble': '', 'title': '', 'date': '', @@ -234,6 +236,14 @@ class LaTeXTranslator(nodes.NodeVisitor): self.elements['fncychap'] = '' else: self.elements['classoptions'] += ',english' + if getattr(builder, 'usepackages', None): + def declare_package(packagename, options=None): + if options: + return '\\usepackage[%s]{%s}' % (options, packagename) + else: + return '\\usepackage{%s}' % (packagename,) + usepackages = (declare_package(*p) for p in builder.usepackages) + self.elements['usepackages'] += "\n".join(usepackages) # allow the user to override them all self.elements.update(builder.config.latex_elements) if self.elements['extraclassoptions']: diff --git a/tests/roots/test-intl/conf.py b/tests/roots/test-intl/conf.py index 4c37f771..1b20244c 100644 --- a/tests/roots/test-intl/conf.py +++ b/tests/roots/test-intl/conf.py @@ -6,3 +6,4 @@ keep_warnings = True templates_path = ['_templates'] html_additional_pages = {'index': 'index.html'} release = version = '2013.120' +gettext_enables = ['index'] diff --git a/tests/roots/test-numfig/bar.rst b/tests/roots/test-numfig/bar.rst new file mode 100644 index 00000000..f86e7475 --- /dev/null +++ b/tests/roots/test-numfig/bar.rst @@ -0,0 +1,58 @@ +=== +Bar +=== + +Bar A +===== + +.. figure:: rimg.png + + should be Fig.2.1 + +.. csv-table:: should be Table 2.1 + :header-rows: 0 + + hello,world + +.. code-block:: python + :caption: should be List 2.1 + + print('hello world') + +.. toctree:: + + baz + +.. figure:: rimg.png + + should be Fig.2.3 + +.. csv-table:: should be Table 2.3 + :header-rows: 0 + + hello,world + +.. code-block:: python + :caption: should be List 2.3 + + print('hello world') + +Bar B +===== + +Bar B1 +------ + +.. figure:: rimg.png + + should be Fig.2.4 + +.. csv-table:: should be Table 2.4 + :header-rows: 0 + + hello,world + +.. code-block:: python + :caption: should be List 2.4 + + print('hello world') diff --git a/tests/roots/test-numfig/baz.rst b/tests/roots/test-numfig/baz.rst new file mode 100644 index 00000000..9e2ccfeb --- /dev/null +++ b/tests/roots/test-numfig/baz.rst @@ -0,0 +1,16 @@ +Baz A +----- + +.. figure:: rimg.png + + should be Fig.2.2 + +.. csv-table:: should be Table 2.2 + :header-rows: 0 + + hello,world + +.. code-block:: python + :caption: should be List 2.2 + + print('hello world') diff --git a/tests/roots/test-numfig/conf.py b/tests/roots/test-numfig/conf.py new file mode 100644 index 00000000..f81c30bc --- /dev/null +++ b/tests/roots/test-numfig/conf.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +master_doc = 'index' diff --git a/tests/roots/test-numfig/foo.rst b/tests/roots/test-numfig/foo.rst new file mode 100644 index 00000000..ef713574 --- /dev/null +++ b/tests/roots/test-numfig/foo.rst @@ -0,0 +1,71 @@ +=== +Foo +=== + +.. figure:: rimg.png + + should be Fig.1.1 + +.. csv-table:: should be Table 1.1 + :header-rows: 0 + + hello,world + +.. code-block:: python + :caption: should be List 1.1 + + print('hello world') + +Foo A +===== + +.. figure:: rimg.png + + should be Fig.1.2 + +.. figure:: rimg.png + + should be Fig.1.3 + +.. csv-table:: should be Table 1.2 + :header-rows: 0 + + hello,world + +.. csv-table:: should be Table 1.3 + :header-rows: 0 + + hello,world + +.. code-block:: python + :caption: should be List 1.2 + + print('hello world') + +.. code-block:: python + :caption: should be List 1.3 + + print('hello world') + +Foo A1 +------ + +Foo B +===== + +Foo B1 +------ + +.. figure:: rimg.png + + should be Fig.1.4 + +.. csv-table:: should be Table 1.4 + :header-rows: 0 + + hello,world + +.. code-block:: python + :caption: should be List 1.4 + + print('hello world') diff --git a/tests/roots/test-numfig/index.rst b/tests/roots/test-numfig/index.rst new file mode 100644 index 00000000..564018e3 --- /dev/null +++ b/tests/roots/test-numfig/index.rst @@ -0,0 +1,36 @@ +test-tocdepth +============= + +.. toctree:: + :numbered: + + foo + bar + +.. figure:: rimg.png + + should be Fig.1 + +.. figure:: rimg.png + + should be Fig.2 + +.. csv-table:: should be Table 1 + :header-rows: 0 + + hello,world + +.. csv-table:: should be Table 2 + :header-rows: 0 + + hello,world + +.. code-block:: python + :caption: should be List 1 + + print('hello world') + +.. code-block:: python + :caption: should be List 2 + + print('hello world') diff --git a/tests/roots/test-numfig/rimg.png b/tests/roots/test-numfig/rimg.png Binary files differnew file mode 100644 index 00000000..1081dc14 --- /dev/null +++ b/tests/roots/test-numfig/rimg.png diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index d7189443..1de1306e 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -113,6 +113,38 @@ def test_gettext_index_entries(app, status, warning): "Exception", "Statement", "Builtin", + ] + for expect in expected_msgids: + assert expect in msgids + msgids.remove(expect) + + # unexpected msgid existent + assert msgids == [] + + +@with_app('gettext', testroot='intl', + confoverrides={'gettext_compact': False, 'gettext_enables': []}) +def test_gettext_disable_index_entries(app, status, warning): + # regression test for #976 + app.builder.build(['index_entries']) + + _msgid_getter = re.compile(r'msgid "(.*)"').search + + def msgid_getter(msgid): + m = _msgid_getter(msgid) + if m: + return m.groups()[0] + return None + + pot = (app.outdir / 'index_entries.pot').text(encoding='utf-8') + msgids = [_f for _f in map(msgid_getter, pot.splitlines()) if _f] + + expected_msgids = [ + "i18n with index entries", + "index target section", + "this is :index:`Newsletter` target paragraph.", + "various index entries", + "That's all.", ] for expect in expected_msgids: assert expect in msgids diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 52604468..ae518062 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -455,3 +455,402 @@ def test_tocdepth_singlehtml(app, status, warning): for xpath, check, be_found in paths: yield check_xpath, etree, fname, xpath, check, be_found + + +@gen_with_app(buildername='html', testroot='numfig') +def test_numfig(app, status, warning): + # issue #1251 + app.builder.build_all() + + expects = { + 'index.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.2$', True), + (".//table/caption", '^should be Table 1$', True), + (".//table/caption", '^should be Table 2$', True), + (".//div[@class='code-block-caption']", + '^should be List 1$', True), + (".//div[@class='code-block-caption']", + '^should be List 2$', True), + ], + 'foo.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.1.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.1.2$', True), + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.1.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.1.4$', True), + (".//table/caption", '^should be Table 1.1$', True), + (".//table/caption", '^should be Table 1.2$', True), + (".//table/caption", '^should be Table 1.3$', True), + (".//table/caption", '^should be Table 1.4$', True), + (".//div[@class='code-block-caption']", + '^should be List 1.1$', True), + (".//div[@class='code-block-caption']", + '^should be List 1.2$', True), + (".//div[@class='code-block-caption']", + '^should be List 1.3$', True), + (".//div[@class='code-block-caption']", + '^should be List 1.4$', True), + ], + 'bar.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.2.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.2.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.2.4$', True), + (".//table/caption", '^should be Table 2.1$', True), + (".//table/caption", '^should be Table 2.3$', True), + (".//table/caption", '^should be Table 2.4$', True), + (".//div[@class='code-block-caption']", + '^should be List 2.1$', True), + (".//div[@class='code-block-caption']", + '^should be List 2.3$', True), + (".//div[@class='code-block-caption']", + '^should be List 2.4$', True), + ], + 'baz.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^should be Fig.2.2$', True), + (".//table/caption", '^should be Table 2.2$', True), + (".//div[@class='code-block-caption']", + '^should be List 2.2$', True), + ], + } + + for fname, paths in iteritems(expects): + parser = NslessParser() + parser.entity.update(html_entities.entitydefs) + fp = open(os.path.join(app.outdir, fname), 'rb') + try: + etree = ET.parse(fp, parser) + finally: + fp.close() + + for xpath, check, be_found in paths: + yield check_xpath, etree, fname, xpath, check, be_found + + +@gen_with_app(buildername='html', testroot='numfig', + confoverrides={'numfig': True}) +def test_numfig_without_numbered_toctree(app, status, warning): + # remove :numbered: option + index = (app.srcdir / 'index.rst').text() + index = re.sub(':numbered:.*', '', index, re.MULTILINE) + (app.srcdir / 'index.rst').write_text(index, encoding='utf-8') + app.builder.build_all() + + expects = { + 'index.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.9 should be Fig.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.10 should be Fig.2$', True), + (".//table/caption", '^Table 9 should be Table 1$', True), + (".//table/caption", '^Table 10 should be Table 2$', True), + (".//div[@class='code-block-caption']", + '^List 9 should be List 1$', True), + (".//div[@class='code-block-caption']", + '^List 10 should be List 2$', True), + ], + 'foo.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1 should be Fig.1.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2 should be Fig.1.2$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.3 should be Fig.1.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.4 should be Fig.1.4$', True), + (".//table/caption", '^Table 1 should be Table 1.1$', True), + (".//table/caption", '^Table 2 should be Table 1.2$', True), + (".//table/caption", '^Table 3 should be Table 1.3$', True), + (".//table/caption", '^Table 4 should be Table 1.4$', True), + (".//div[@class='code-block-caption']", + '^List 1 should be List 1.1$', True), + (".//div[@class='code-block-caption']", + '^List 2 should be List 1.2$', True), + (".//div[@class='code-block-caption']", + '^List 3 should be List 1.3$', True), + (".//div[@class='code-block-caption']", + '^List 4 should be List 1.4$', True), + ], + 'bar.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.5 should be Fig.2.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.7 should be Fig.2.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.8 should be Fig.2.4$', True), + (".//table/caption", '^Table 5 should be Table 2.1$', True), + (".//table/caption", '^Table 7 should be Table 2.3$', True), + (".//table/caption", '^Table 8 should be Table 2.4$', True), + (".//div[@class='code-block-caption']", + '^List 5 should be List 2.1$', True), + (".//div[@class='code-block-caption']", + '^List 7 should be List 2.3$', True), + (".//div[@class='code-block-caption']", + '^List 8 should be List 2.4$', True), + ], + 'baz.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.6 should be Fig.2.2$', True), + (".//table/caption", '^Table 6 should be Table 2.2$', True), + (".//div[@class='code-block-caption']", + '^List 6 should be List 2.2$', True), + ], + } + + for fname, paths in iteritems(expects): + parser = NslessParser() + parser.entity.update(html_entities.entitydefs) + fp = open(os.path.join(app.outdir, fname), 'rb') + try: + etree = ET.parse(fp, parser) + finally: + fp.close() + + for xpath, check, be_found in paths: + yield check_xpath, etree, fname, xpath, check, be_found + + +@gen_with_app(buildername='html', testroot='numfig', + confoverrides={'numfig': True}) +def test_numfig_with_numbered_toctree(app, status, warning): + app.builder.build_all() + + expects = { + 'index.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1 should be Fig.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2 should be Fig.2$', True), + (".//table/caption", '^Table 1 should be Table 1$', True), + (".//table/caption", '^Table 2 should be Table 2$', True), + (".//div[@class='code-block-caption']", + '^List 1 should be List 1$', True), + (".//div[@class='code-block-caption']", + '^List 2 should be List 2$', True), + ], + 'foo.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1.1 should be Fig.1.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1.2 should be Fig.1.2$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1.3 should be Fig.1.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1.4 should be Fig.1.4$', True), + (".//table/caption", '^Table 1.1 should be Table 1.1$', True), + (".//table/caption", '^Table 1.2 should be Table 1.2$', True), + (".//table/caption", '^Table 1.3 should be Table 1.3$', True), + (".//table/caption", '^Table 1.4 should be Table 1.4$', True), + (".//div[@class='code-block-caption']", + '^List 1.1 should be List 1.1$', True), + (".//div[@class='code-block-caption']", + '^List 1.2 should be List 1.2$', True), + (".//div[@class='code-block-caption']", + '^List 1.3 should be List 1.3$', True), + (".//div[@class='code-block-caption']", + '^List 1.4 should be List 1.4$', True), + ], + 'bar.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2.1 should be Fig.2.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2.3 should be Fig.2.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2.4 should be Fig.2.4$', True), + (".//table/caption", '^Table 2.1 should be Table 2.1$', True), + (".//table/caption", '^Table 2.3 should be Table 2.3$', True), + (".//table/caption", '^Table 2.4 should be Table 2.4$', True), + (".//div[@class='code-block-caption']", + '^List 2.1 should be List 2.1$', True), + (".//div[@class='code-block-caption']", + '^List 2.3 should be List 2.3$', True), + (".//div[@class='code-block-caption']", + '^List 2.4 should be List 2.4$', True), + ], + 'baz.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2.2 should be Fig.2.2$', True), + (".//table/caption", '^Table 2.2 should be Table 2.2$', True), + (".//div[@class='code-block-caption']", + '^List 2.2 should be List 2.2$', True), + ], + } + + for fname, paths in iteritems(expects): + parser = NslessParser() + parser.entity.update(html_entities.entitydefs) + fp = open(os.path.join(app.outdir, fname), 'rb') + try: + etree = ET.parse(fp, parser) + finally: + fp.close() + + for xpath, check, be_found in paths: + yield check_xpath, etree, fname, xpath, check, be_found + + +@gen_with_app(buildername='html', testroot='numfig', + confoverrides={'numfig': True, 'numfig_prefix': {'figure': 'Figure:', 'table': 'Tab_', 'code-block': 'Code-'}}) +def test_numfig_with_prefix(app, status, warning): + app.builder.build_all() + + expects = { + 'index.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Figure:1 should be Fig.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Figure:2 should be Fig.2$', True), + (".//table/caption", '^Tab_1 should be Table 1$', True), + (".//table/caption", '^Tab_2 should be Table 2$', True), + (".//div[@class='code-block-caption']", + '^Code-1 should be List 1$', True), + (".//div[@class='code-block-caption']", + '^Code-2 should be List 2$', True), + ], + 'foo.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Figure:1.1 should be Fig.1.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Figure:1.2 should be Fig.1.2$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Figure:1.3 should be Fig.1.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Figure:1.4 should be Fig.1.4$', True), + (".//table/caption", '^Tab_1.1 should be Table 1.1$', True), + (".//table/caption", '^Tab_1.2 should be Table 1.2$', True), + (".//table/caption", '^Tab_1.3 should be Table 1.3$', True), + (".//table/caption", '^Tab_1.4 should be Table 1.4$', True), + (".//div[@class='code-block-caption']", + '^Code-1.1 should be List 1.1$', True), + (".//div[@class='code-block-caption']", + '^Code-1.2 should be List 1.2$', True), + (".//div[@class='code-block-caption']", + '^Code-1.3 should be List 1.3$', True), + (".//div[@class='code-block-caption']", + '^Code-1.4 should be List 1.4$', True), + ], + 'bar.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Figure:2.1 should be Fig.2.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Figure:2.3 should be Fig.2.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Figure:2.4 should be Fig.2.4$', True), + (".//table/caption", '^Tab_2.1 should be Table 2.1$', True), + (".//table/caption", '^Tab_2.3 should be Table 2.3$', True), + (".//table/caption", '^Tab_2.4 should be Table 2.4$', True), + (".//div[@class='code-block-caption']", + '^Code-2.1 should be List 2.1$', True), + (".//div[@class='code-block-caption']", + '^Code-2.3 should be List 2.3$', True), + (".//div[@class='code-block-caption']", + '^Code-2.4 should be List 2.4$', True), + ], + 'baz.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Figure:2.2 should be Fig.2.2$', True), + (".//table/caption", '^Tab_2.2 should be Table 2.2$', True), + (".//div[@class='code-block-caption']", + '^Code-2.2 should be List 2.2$', True), + ], + } + + for fname, paths in iteritems(expects): + parser = NslessParser() + parser.entity.update(html_entities.entitydefs) + fp = open(os.path.join(app.outdir, fname), 'rb') + try: + etree = ET.parse(fp, parser) + finally: + fp.close() + + for xpath, check, be_found in paths: + yield check_xpath, etree, fname, xpath, check, be_found + + +@gen_with_app(buildername='html', testroot='numfig', + confoverrides={'numfig': True, 'numfig_secnum_depth': 2}) +def test_numfig_with_secnum_depth(app, status, warning): + app.builder.build_all() + + expects = { + 'index.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1 should be Fig.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2 should be Fig.2$', True), + (".//table/caption", '^Table 1 should be Table 1$', True), + (".//table/caption", '^Table 2 should be Table 2$', True), + (".//div[@class='code-block-caption']", + '^List 1 should be List 1$', True), + (".//div[@class='code-block-caption']", + '^List 2 should be List 2$', True), + ], + 'foo.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1.1 should be Fig.1.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1.1.1 should be Fig.1.2$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1.1.2 should be Fig.1.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.1.2.1 should be Fig.1.4$', True), + (".//table/caption", '^Table 1.1 should be Table 1.1$', True), + (".//table/caption", '^Table 1.1.1 should be Table 1.2$', True), + (".//table/caption", '^Table 1.1.2 should be Table 1.3$', True), + (".//table/caption", '^Table 1.2.1 should be Table 1.4$', True), + (".//div[@class='code-block-caption']", + '^List 1.1 should be List 1.1$', True), + (".//div[@class='code-block-caption']", + '^List 1.1.1 should be List 1.2$', True), + (".//div[@class='code-block-caption']", + '^List 1.1.2 should be List 1.3$', True), + (".//div[@class='code-block-caption']", + '^List 1.2.1 should be List 1.4$', True), + ], + 'bar.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2.1.1 should be Fig.2.1$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2.1.3 should be Fig.2.3$', True), + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2.2.1 should be Fig.2.4$', True), + (".//table/caption", '^Table 2.1.1 should be Table 2.1$', True), + (".//table/caption", '^Table 2.1.3 should be Table 2.3$', True), + (".//table/caption", '^Table 2.2.1 should be Table 2.4$', True), + (".//div[@class='code-block-caption']", + '^List 2.1.1 should be List 2.1$', True), + (".//div[@class='code-block-caption']", + '^List 2.1.3 should be List 2.3$', True), + (".//div[@class='code-block-caption']", + '^List 2.2.1 should be List 2.4$', True), + ], + 'baz.html': [ + (".//div[@class='figure']/p[@class='caption']", + '^Fig.2.1.2 should be Fig.2.2$', True), + (".//table/caption", '^Table 2.1.2 should be Table 2.2$', True), + (".//div[@class='code-block-caption']", + '^List 2.1.2 should be List 2.2$', True), + ], + } + + for fname, paths in iteritems(expects): + parser = NslessParser() + parser.entity.update(html_entities.entitydefs) + fp = open(os.path.join(app.outdir, fname), 'rb') + try: + etree = ET.parse(fp, parser) + finally: + fp.close() + + for xpath, check, be_found in paths: + yield check_xpath, etree, fname, xpath, check, be_found diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 9e4c11d5..374b54a2 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -91,3 +91,13 @@ def test_latex(app, status, warning): assert False, 'latex exited with return code %s' % p.returncode finally: os.chdir(cwd) + + +@with_app(buildername='latex') +def test_latex_add_latex_package(app, status, warning): + app.add_latex_package('foo') + app.add_latex_package('bar', 'baz') + app.builder.build_all() + result = (app.outdir / 'SphinxTests.tex').text(encoding='utf8') + assert '\\usepackage{foo}' in result + assert '\\usepackage[baz]{bar}' in result |