mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
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:
commit
2826af738d
@ -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
|
||||
-------------------
|
||||
|
@ -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
|
||||
-----------------------------------------
|
||||
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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, ('', '', ''))
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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>`
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>`
|
||||
|
@ -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']/"
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user