diff --git a/doc/config.rst b/doc/config.rst index 0cc0ceba0..e7e2455dd 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -231,6 +231,30 @@ General configuration .. versionadded:: 1.1 +.. confval:: numfig + + If true, figures, tables and code-blocks are automatically numbered if they + has caption. For now, it works only with the HTML builder. Default is ``False``. + + .. versionadded:: 1.3 + +.. confval:: numfig_prefix + + A dictionary mapping ``'figure'``, ``'table'`` and ``'code-block'`` to + strings that are used for prefix of figure numbers. Default is to use + ``'Fig. %s'`` for ``'figure'``, ``'Table %s'`` for ``'table'`` and + ``'Listing %s'`` for ``'code-block'``. + + .. versionadded:: 1.3 + +.. confval:: numfig_secnum_depth + + The scope of figure numbers, that is, the numfig feature numbers figures + in which scope. ``0`` means "whole document". ``1`` means "in a section". + Sphinx numbers like x.1, x.2, x.3... ``2`` means "in a subsection". Sphinx + numbers like x.x.1, x.x.2, x.x.3..., and so on. Default is ``1``. + + .. versionadded:: 1.3 Project information ------------------- diff --git a/doc/markup/inline.rst b/doc/markup/inline.rst index b5bb8d0c5..6f3ebe1c4 100644 --- a/doc/markup/inline.rst +++ b/doc/markup/inline.rst @@ -201,6 +201,24 @@ Referencing downloadable files suitable link generated to it. +Cross-referencing figures by figure number +------------------------------------------ + +.. versionadded:: 1.3 + +.. rst:role:: numref + + Link to the specified figures, tables and code-blocks; the standard reST + labels are used. When you use this role, it will insert a reference to the + figure with link text by its figure number like "Fig. 1.1". + + If an explicit link text is given (like usual: ``:doc:`Image of Sphinx (Fig. + #) ```), the link caption will be the title of the reference. + As a special character, `#` will be replaced to figure number. + + If :confval:`numfig` is ``False``, figures are not numbered. + so this role inserts not a reference but labels or link text. + Cross-referencing other items of interest ----------------------------------------- diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index 9d8c46901..362abd5a4 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -184,6 +184,10 @@ class pending_xref(nodes.Inline, nodes.Element): """ +class number_reference(nodes.reference): + """Node for number references, similar to pending_xref.""" + + class download_reference(nodes.reference): """Node for download references, similar to pending_xref.""" diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 0338a6b57..03228da2b 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -22,7 +22,7 @@ from sphinx.roles import XRefRole from sphinx.locale import l_, _ from sphinx.domains import Domain, ObjType from sphinx.directives import ObjectDescription -from sphinx.util import ws_re +from sphinx.util import ws_re, get_figtype from sphinx.util.nodes import clean_astext, make_refnode from sphinx.util.compat import Directive @@ -466,6 +466,9 @@ class StandardDomain(Domain): # links to headings or arbitrary labels 'ref': XRefRole(lowercase=True, innernodeclass=nodes.emphasis, warn_dangling=True), + # links to labels of numbered figures, tables and code-blocks + 'numref': XRefRole(lowercase=True, + warn_dangling=True), # links to labels, without a different title 'keyword': XRefRole(warn_dangling=True), } @@ -489,6 +492,7 @@ class StandardDomain(Domain): 'term': 'term not in glossary: %(target)s', 'ref': 'undefined label: %(target)s (if the link has no caption ' 'the label must precede a section header)', + 'numref': 'undefined label: %(target)s', 'keyword': 'unknown keyword: %(target)s', } @@ -574,6 +578,28 @@ class StandardDomain(Domain): continue labels[name] = docname, labelid, sectname + def build_reference_node(self, fromdocname, builder, + docname, labelid, sectname, + **options): + nodeclass = options.pop('nodeclass', nodes.reference) + newnode = nodeclass('', '', internal=True, **options) + innernode = nodes.emphasis(sectname, sectname) + if docname == fromdocname: + newnode['refid'] = labelid + else: + # set more info in contnode; in case the + # get_relative_uri call raises NoUri, + # the builder will then have to resolve these + contnode = addnodes.pending_xref('') + contnode['refdocname'] = docname + contnode['refsectname'] = sectname + newnode['refuri'] = builder.get_relative_uri( + fromdocname, docname) + if labelid: + newnode['refuri'] += '#' + labelid + newnode.append(innernode) + return newnode + def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): if typ == 'ref': @@ -589,23 +615,38 @@ class StandardDomain(Domain): ('', '', '')) if not docname: return None - newnode = nodes.reference('', '', internal=True) - innernode = nodes.emphasis(sectname, sectname) - if docname == fromdocname: - newnode['refid'] = labelid + + return self.build_reference_node(fromdocname, builder, + docname, labelid, sectname) + elif typ == 'numref': + docname, labelid = self.data['anonlabels'].get(target, ('', '')) + if not docname: + return None + + if env.config.numfig is False: + env.warn(fromdocname, 'numfig is disabled. :numref: is ignored.') + return contnode + + try: + target = env.get_doctree(docname).ids[labelid] + figtype = get_figtype(target) + figure_id = target['ids'][0] + fignumber = env.toc_fignumbers[docname][figtype][figure_id] + except (KeyError, IndexError): + return None + + title = contnode.astext() + if labelid == title: + prefix = env.config.numfig_prefix.get(figtype, '') + title = prefix.replace('%s', '#') + newtitle = prefix % '.'.join(map(str, fignumber)) else: - # set more info in contnode; in case the - # get_relative_uri call raises NoUri, - # the builder will then have to resolve these - contnode = addnodes.pending_xref('') - contnode['refdocname'] = docname - contnode['refsectname'] = sectname - newnode['refuri'] = builder.get_relative_uri( - fromdocname, docname) - if labelid: - newnode['refuri'] += '#' + labelid - newnode.append(innernode) - return newnode + newtitle = title.replace('#', '.'.join(map(str, fignumber))) + + return self.build_reference_node(fromdocname, builder, + docname, labelid, newtitle, + nodeclass=addnodes.number_reference, + title=title) elif typ == 'keyword': # keywords are oddballs: they are referenced by named labels docname, labelid, _ = self.data['labels'].get(target, ('', '', '')) diff --git a/sphinx/environment.py b/sphinx/environment.py index 534f34920..f0021fd31 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -37,7 +37,7 @@ from docutils.frontend import OptionParser from sphinx import addnodes from sphinx.util import url_re, get_matching_docs, docname_join, split_into, \ - FilenameUniqDict + FilenameUniqDict, get_figtype from sphinx.util.nodes import clean_astext, make_refnode, WarningStream from sphinx.util.osutil import SEP, find_catalog_files, getcwd, fs_encoding from sphinx.util.console import bold, purple @@ -1710,9 +1710,6 @@ class BuildEnvironment: 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, {}) @@ -1754,16 +1751,10 @@ class BuildEnvironment: 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) + figtype = get_figtype(subnode) + if figtype and subnode['ids']: + register_fignumber(docname, secnum, + figtype, subnode['ids'][0]) _walk_doctree(docname, subnode, secnum) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index e7277520b..024e25b1b 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -475,3 +475,20 @@ class PeekableIterator(object): item = next(self) self.push(item) return item + + +def get_figtype(node): + """Return figtype for given node.""" + def has_child(node, cls): + return any(isinstance(child, cls) for child in node) + + from docutils import nodes + if isinstance(node, nodes.figure): + return 'figure' + elif isinstance(node, nodes.table): + return 'table' + elif isinstance(node, nodes.container): + if has_child(node, nodes.literal_block): + return 'code-block' + + return None diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 09619f9f2..836464a33 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -207,6 +207,12 @@ class HTMLTranslator(BaseTranslator): self.body.append(('%s' + self.secnumber_suffix) % '.'.join(map(str, node['secnumber']))) + def visit_number_reference(self, node): + self.visit_reference(node) + + def depart_number_reference(self, node): + self.depart_reference(node) + # overwritten -- we don't want source comments to show up in the HTML def visit_comment(self, node): raise nodes.SkipNode diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index ea79761fe..dd85a8065 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -682,6 +682,9 @@ class LaTeXTranslator(nodes.NodeVisitor): if not self.table.longtable and self.table.caption is not None: self.body.append(u'\n\n\\begin{threeparttable}\n' u'\\capstart\\caption{%s}\n' % self.table.caption) + for id in self.next_table_ids: + self.body.append(self.hypertarget(id, anchor=False)) + self.next_table_ids.clear() if self.table.longtable: self.body.append('\n\\begin{longtable}') endmacro = '\\end{longtable}\n\n' @@ -709,11 +712,11 @@ class LaTeXTranslator(nodes.NodeVisitor): else: self.body.append('{|' + ('L|' * self.table.colcount) + '}\n') if self.table.longtable and self.table.caption is not None: - self.body.append(u'\\caption{%s} \\\\\n' % self.table.caption) - if self.table.caption is not None: + self.body.append(u'\\caption{%s}' % self.table.caption) for id in self.next_table_ids: self.body.append(self.hypertarget(id, anchor=False)) self.next_table_ids.clear() + self.body.append(u'\\\\\n') if self.table.longtable: self.body.append('\\hline\n') self.body.extend(self.tableheaders) @@ -1114,7 +1117,7 @@ class LaTeXTranslator(nodes.NodeVisitor): return elif isinstance(next, nodes.table): # same for tables, but only if they have a caption - for n in node: + for n in next: if isinstance(n, nodes.title): if node.get('refid'): self.next_table_ids.add(node['refid']) @@ -1244,6 +1247,18 @@ class LaTeXTranslator(nodes.NodeVisitor): def depart_reference(self, node): self.body.append(self.context.pop()) + def visit_number_reference(self, node): + if node.get('refid'): + id = self.curfilestack[-1] + ':' + node['refid'] + else: + id = node.get('refuri', '')[1:].replace('#', ':') + + ref = '\\ref{%s}' % self.idescape(id) + title = node.get('title', '#') + self.body.append(title.replace('#', ref)) + + raise nodes.SkipNode + def visit_download_reference(self, node): pass def depart_download_reference(self, node): @@ -1511,11 +1526,12 @@ class LaTeXTranslator(nodes.NodeVisitor): for id in self.next_literal_ids: ids += self.hypertarget(id, anchor=False) self.next_literal_ids.clear() - self.body.append('\n\\begin{literal-block}' + ids) + self.body.append('\n\\begin{literal-block}\n') + self.context.append(ids + '\n\\end{literal-block}\n') def depart_container(self, node): if node.get('literal_block'): - self.body.append('\\end{literal-block}\n') + self.body.append(self.context.pop()) def visit_decoration(self, node): pass diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index 8d49f807f..bc8420570 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -246,6 +246,11 @@ class ManualPageTranslator(BaseTranslator): '>']) raise nodes.SkipNode + def visit_number_reference(self, node): + text = nodes.Text(node.get('title', '#')) + self.visit_Text(text) + raise nodes.SkipNode + def visit_centered(self, node): self.ensure_eol() self.body.append('.sp\n.ce\n') diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 61d2e62f2..93d4d6aa5 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -722,6 +722,11 @@ class TexinfoTranslator(nodes.NodeVisitor): def depart_reference(self, node): pass + def visit_number_reference(self, node): + text = nodes.Text(node.get('title', '#')) + self.visit_Text(text) + raise nodes.SkipNode + def visit_title_reference(self, node): text = node.astext() self.body.append('@cite{%s}' % self.escape_arg(text)) diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 5b32b05ab..efd32df3a 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -748,6 +748,11 @@ class TextTranslator(nodes.NodeVisitor): def depart_reference(self, node): pass + def visit_number_reference(self, node): + text = nodes.Text(node.get('title', '#')) + self.visit_Text(text) + raise nodes.SkipNode + def visit_download_reference(self, node): pass def depart_download_reference(self, node): diff --git a/tests/root/conf.py b/tests/root/conf.py index d12e8167f..5186f3715 100644 --- a/tests/root/conf.py +++ b/tests/root/conf.py @@ -24,6 +24,7 @@ exclude_patterns = ['_build', '**/excluded.*'] keep_warnings = True pygments_style = 'sphinx' show_authors = True +numfig = True rst_epilog = '.. |subst| replace:: global substitution' diff --git a/tests/root/markup.txt b/tests/root/markup.txt index 6ed396ac0..1ad7a13b6 100644 --- a/tests/root/markup.txt +++ b/tests/root/markup.txt @@ -143,6 +143,9 @@ Adding \n to test unescaping. * :ref:`my-figure` * :ref:`my-table` * :ref:`my-code-block` +* :numref:`my-figure` +* :numref:`my-table` +* :numref:`my-code-block` * :doc:`subdir/includes` * ``:download:`` is tested in includes.txt * :option:`Python -c option ` diff --git a/tests/roots/test-numfig/baz.rst b/tests/roots/test-numfig/baz.rst index 9e2ccfebb..da9e0fe0a 100644 --- a/tests/roots/test-numfig/baz.rst +++ b/tests/roots/test-numfig/baz.rst @@ -1,15 +1,21 @@ Baz A ----- +.. _fig22: + .. figure:: rimg.png should be Fig.2.2 +.. _table22: + .. csv-table:: should be Table 2.2 :header-rows: 0 hello,world +.. _code22: + .. code-block:: python :caption: should be List 2.2 diff --git a/tests/roots/test-numfig/index.rst b/tests/roots/test-numfig/index.rst index 564018e36..7bdf135c8 100644 --- a/tests/roots/test-numfig/index.rst +++ b/tests/roots/test-numfig/index.rst @@ -7,6 +7,8 @@ test-tocdepth foo bar +.. _fig1: + .. figure:: rimg.png should be Fig.1 @@ -15,6 +17,8 @@ test-tocdepth should be Fig.2 +.. _table1: + .. csv-table:: should be Table 1 :header-rows: 0 @@ -25,6 +29,8 @@ test-tocdepth hello,world +.. _code1: + .. code-block:: python :caption: should be List 1 @@ -34,3 +40,11 @@ test-tocdepth :caption: should be List 2 print('hello world') + + +* Fig.1 is :numref:`fig1` +* Fig.2.2 is :numref:`Figure# ` +* Table.1 is :numref:`table1` +* Table.2.2 is :numref:`Table:# ` +* List.1 is :numref:`code1` +* List.2.2 is :numref:`Code-# ` diff --git a/tests/test_build_html.py b/tests/test_build_html.py index f5c9a38e3..baf3b6a6c 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -463,8 +463,7 @@ def test_tocdepth_singlehtml(app, status, warning): @gen_with_app(buildername='html', testroot='numfig') -def test_numfig(app, status, warning): - # issue #1251 +def test_numfig_disabled(app, status, warning): app.builder.build_all() expects = { @@ -474,6 +473,12 @@ def test_numfig(app, status, warning): (".//table/caption/span[@class='caption-number']", None, True), (".//div[@class='code-block-caption']/" "span[@class='caption-number']", None, True), + (".//li/code/span", '^fig1$', True), + (".//li/code/span", '^Figure#$', True), + (".//li/code/span", '^table1$', True), + (".//li/code/span", '^Table:#$', True), + (".//li/code/span", '^code1$', True), + (".//li/code/span", '^Code-#$', True), ], 'foo.html': [ (".//div[@class='figure']/p[@class='caption']/" @@ -534,6 +539,12 @@ def test_numfig_without_numbered_toctree(app, status, warning): "span[@class='caption-number']", '^Listing 9 $', True), (".//div[@class='code-block-caption']/" "span[@class='caption-number']", '^Listing 10 $', True), + (".//li/a/em", '^Fig. 9$', True), + (".//li/a/em", '^Figure6$', True), + (".//li/a/em", '^Table 9$', True), + (".//li/a/em", '^Table:6$', True), + (".//li/a/em", '^Listing 9$', True), + (".//li/a/em", '^Code-6$', True), ], 'foo.html': [ (".//div[@class='figure']/p[@class='caption']/" @@ -623,6 +634,12 @@ def test_numfig_with_numbered_toctree(app, status, warning): "span[@class='caption-number']", '^Listing 1 $', True), (".//div[@class='code-block-caption']/" "span[@class='caption-number']", '^Listing 2 $', True), + (".//li/a/em", '^Fig. 1$', True), + (".//li/a/em", '^Figure2.2$', True), + (".//li/a/em", '^Table 1$', True), + (".//li/a/em", '^Table:2.2$', True), + (".//li/a/em", '^Listing 1$', True), + (".//li/a/em", '^Code-2.2$', True), ], 'foo.html': [ (".//div[@class='figure']/p[@class='caption']/" @@ -715,6 +732,12 @@ def test_numfig_with_prefix(app, status, warning): "span[@class='caption-number']", '^Code-1 $', True), (".//div[@class='code-block-caption']/" "span[@class='caption-number']", '^Code-2 $', True), + (".//li/a/em", '^Figure:1$', True), + (".//li/a/em", '^Figure2.2$', True), + (".//li/a/em", '^Tab_1$', True), + (".//li/a/em", '^Table:2.2$', True), + (".//li/a/em", '^Code-1$', True), + (".//li/a/em", '^Code-2.2$', True), ], 'foo.html': [ (".//div[@class='figure']/p[@class='caption']/" @@ -804,6 +827,12 @@ def test_numfig_with_secnum_depth(app, status, warning): "span[@class='caption-number']", '^Listing 1 $', True), (".//div[@class='code-block-caption']/" "span[@class='caption-number']", '^Listing 2 $', True), + (".//li/a/em", '^Fig. 1$', True), + (".//li/a/em", '^Figure2.1.2$', True), + (".//li/a/em", '^Table 1$', True), + (".//li/a/em", '^Table:2.1.2$', True), + (".//li/a/em", '^Listing 1$', True), + (".//li/a/em", '^Code-2.1.2$', True), ], 'foo.html': [ (".//div[@class='figure']/p[@class='caption']/" diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 374b54a22..3b6dd3e22 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -93,6 +93,22 @@ def test_latex(app, status, warning): os.chdir(cwd) +@with_app(buildername='latex', testroot='numfig', + confoverrides={'numfig': True}) +def test_numref(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'Python.tex').text(encoding='utf8') + print(result) + print(status.getvalue()) + print(warning.getvalue()) + assert '\\ref{index:fig1}' in result + assert '\\ref{baz:fig22}' in result + assert '\\ref{index:table1}' in result + assert '\\ref{baz:table22}' in result + assert '\\ref{index:code1}' in result + assert '\\ref{baz:code22}' in result + + @with_app(buildername='latex') def test_latex_add_latex_package(app, status, warning): app.add_latex_package('foo')