Merged in tk0miya/sphinx (pull request #303)

Add :numref: role to refer figures, tables and code-blocks by its fignum
This commit is contained in:
Takayuki Shimizukawa 2014-10-09 19:45:50 +09:00
commit 2826af738d
17 changed files with 239 additions and 38 deletions

View File

@ -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
-------------------

View File

@ -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.
#) <my-figure>```), 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
-----------------------------------------

View File

@ -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."""

View File

@ -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, ('', '', ''))

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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))

View File

@ -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):

View File

@ -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'

View File

@ -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 <python -c>`

View File

@ -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

View File

@ -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# <fig22>`
* Table.1 is :numref:`table1`
* Table.2.2 is :numref:`Table:# <table22>`
* List.1 is :numref:`code1`
* List.2.2 is :numref:`Code-# <code22>`

View File

@ -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']/"

View File

@ -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')