Add domain support to support equation cross references (ref: #1210, #2851)

This commit is contained in:
Takeshi KOMIYA 2016-08-16 23:36:31 +09:00
parent e0d2783c7b
commit bd22d09f48
4 changed files with 132 additions and 72 deletions

View File

@ -101,6 +101,8 @@ Features added
* apidoc now avoids invalidating cached files by not writing to files whose
content doesn't change. This can lead to significant performance wins if
apidoc is run frequently.
* #2851: `sphinx.ext.math` emits missing-reference event if equation not found
* #1210: ``eqref`` role now supports cross reference
Bugs fixed
----------

View File

@ -12,7 +12,10 @@
from docutils import nodes, utils
from docutils.parsers.rst import directives
from sphinx.util.nodes import set_source_info
from sphinx.roles import XRefRole
from sphinx.locale import _
from sphinx.domains import Domain
from sphinx.util.nodes import make_refnode, set_source_info
from sphinx.util.compat import Directive
@ -28,6 +31,76 @@ class eqref(nodes.Inline, nodes.TextElement):
pass
class EqXRefRole(XRefRole):
def result_nodes(self, document, env, node, is_ref):
node['refdomain'] = 'math'
return [node], []
class MathDomain(Domain):
"""Mathematics domain."""
name = 'math'
label = 'mathematics'
initial_data = {
'objects': {}, # labelid -> (docname, eqno)
}
dangling_warnings = {
'eq': 'equation not found: %(target)s',
}
def clear_doc(self, docname):
for labelid, (doc, eqno) in list(self.data['objects'].items()):
if doc == docname:
del self.data['objects'][labelid]
def merge_domaindata(self, docnames, otherdata):
for labelid, (doc, eqno) in otherdata['objects'].items():
if doc in docnames:
self.data['objects'][labelid] = doc
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
assert typ == 'eq'
docname, number = self.data['objects'].get(target, (None, None))
if docname:
if builder.name == 'latex':
newnode = eqref('', **node.attributes)
newnode['docname'] = docname
newnode['target'] = target
return newnode
else:
title = nodes.Text("(%d)" % number)
return make_refnode(builder, fromdocname, docname,
"equation-" + target, title)
else:
return None
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode):
refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode)
if refnode is None:
return []
else:
return [refnode]
def get_objects(self):
return []
def add_equation(self, env, docname, labelid):
equations = self.data['objects']
if labelid in equations:
path = env.doc2path(equations[labelid][0])
msg = _('duplicate label of equation %s, other instance in %s') % (labelid, path)
raise UserWarning(msg)
else:
eqno = self.get_next_equation_number(docname)
equations[labelid] = (docname, eqno)
return eqno
def get_next_equation_number(self, docname):
targets = [eq for eq in self.data['objects'].values() if eq[0] == docname]
return len(targets) + 1
def wrap_displaymath(math, label, numbering):
def is_equation(part):
return part.strip()
@ -68,13 +141,6 @@ def math_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
return [math(latex=latex)], []
def eq_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
text = utils.unescape(text)
node = eqref('(?)', '(?)', target=text)
node['docname'] = inliner.document.settings.env.docname
return [node], []
def is_in_section_title(node):
"""Determine whether the node is in a section title"""
from sphinx.util.nodes import traverse_parent
@ -104,21 +170,47 @@ class MathDirective(Directive):
latex = self.arguments[0] + '\n\n' + latex
node = displaymath()
node['latex'] = latex
node['label'] = self.options.get('name', None)
if node['label'] is None:
node['label'] = self.options.get('label', None)
node['number'] = None
node['label'] = None
if 'name' in self.options:
node['label'] = self.options['name']
if 'label' in self.options:
node['label'] = self.options['label']
node['nowrap'] = 'nowrap' in self.options
node['docname'] = self.state.document.settings.env.docname
ret = [node]
set_source_info(self, node)
if hasattr(self, 'src'):
node.source = self.src
if node['label']:
tnode = nodes.target('', '', ids=['equation-' + node['label']])
self.state.document.note_explicit_target(tnode)
ret.insert(0, tnode)
self.add_target(ret)
return ret
def add_target(self, ret):
node = ret[0]
env = self.state.document.settings.env
# assign label automatically if math_number_all enabled
if node['label'] == '' or (env.config.math_number_all and not node['label']):
seq = env.new_serialno('sphinx.ext.math#equations')
node['label'] = "%s:%d" % (env.docname, seq)
# no targets and numbers are needed
if not node['label']:
return
# register label to domain
domain = env.get_domain('math')
try:
eqno = domain.add_equation(env, env.docname, node['label'])
node['number'] = eqno
# add target node
target = nodes.target('', '', ids=['equation-' + node['label']])
self.state.document.note_explicit_target(target)
ret.insert(0, target)
except UserWarning as exc:
self.state_machine.reporter.warning(exc[0], line=self.lineno)
def latex_visit_math(self, node):
if is_in_section_title(node):
@ -134,14 +226,18 @@ def latex_visit_displaymath(self, node):
if node['nowrap']:
self.body.append(node['latex'])
else:
label = node['label'] and node['docname'] + '-' + node['label'] or None
if not node['label']:
label = None
else:
label = "equation:%s:%s" % (node['docname'], node['label'])
self.body.append(wrap_displaymath(node['latex'], label,
self.builder.config.math_number_all))
raise nodes.SkipNode
def latex_visit_eqref(self, node):
self.body.append('\\eqref{%s-%s}' % (node['docname'], node['target']))
label = "equation:%s:%s" % (node['docname'], node['target'])
self.body.append('\\eqref{%s}' % label)
raise nodes.SkipNode
@ -157,11 +253,6 @@ def text_visit_displaymath(self, node):
raise nodes.SkipNode
def text_visit_eqref(self, node):
self.add_text(node['target'])
raise nodes.SkipNode
def man_visit_math(self, node):
self.body.append(node['latex'])
raise nodes.SkipNode
@ -175,11 +266,6 @@ def man_depart_displaymath(self, node):
self.depart_centered(node)
def man_visit_eqref(self, node):
self.body.append(node['target'])
raise nodes.SkipNode
def texinfo_visit_math(self, node):
self.body.append('@math{' + self.escape_arg(node['latex']) + '}')
raise nodes.SkipNode
@ -196,40 +282,9 @@ def texinfo_depart_displaymath(self, node):
pass
def texinfo_visit_eqref(self, node):
self.add_xref(node['docname'] + ':' + node['target'],
node['target'], node)
raise nodes.SkipNode
def html_visit_eqref(self, node):
self.body.append('<a href="#equation-%s">' % node['target'])
def html_depart_eqref(self, node):
self.body.append('</a>')
def number_equations(app, doctree, docname):
num = 0
numbers = {}
for node in doctree.traverse(displaymath):
if node['label'] is not None or app.config.math_number_all:
num += 1
node['number'] = num
if node['label'] is not None:
numbers[node['label']] = num
else:
node['number'] = None
for node in doctree.traverse(eqref):
if node['target'] not in numbers:
continue
num = '(%d)' % numbers[node['target']]
node[0] = nodes.Text(num, num)
def setup_math(app, htmlinlinevisitors, htmldisplayvisitors):
app.add_config_value('math_number_all', False, 'html')
app.add_config_value('math_number_all', False, 'env')
app.add_domain(MathDomain)
app.add_node(math, override=True,
latex=(latex_visit_math, None),
text=(text_visit_math, None),
@ -242,13 +297,7 @@ def setup_math(app, htmlinlinevisitors, htmldisplayvisitors):
man=(man_visit_displaymath, man_depart_displaymath),
texinfo=(texinfo_visit_displaymath, texinfo_depart_displaymath),
html=htmldisplayvisitors)
app.add_node(eqref,
latex=(latex_visit_eqref, None),
text=(text_visit_eqref, None),
man=(man_visit_eqref, None),
texinfo=(texinfo_visit_eqref, None),
html=(html_visit_eqref, html_depart_eqref))
app.add_node(eqref, latex=(latex_visit_eqref, None))
app.add_role('math', math_role)
app.add_role('eq', eq_role)
app.add_role('eq', EqXRefRole(warn_dangling=True))
app.add_directive('math', MathDirective)
app.connect('doctree-resolved', number_equations)

View File

@ -1,6 +1,10 @@
Test Math
=========
.. toctree::
math
.. math:: a^2+b^2=c^2
Inline :math:`E=mc^2`

View File

@ -25,8 +25,10 @@ def test_jsmath(app, status, warning):
assert (u'<span class="eqno">(1)<a class="headerlink" href="#equation-foo" '
u'title="Permalink to this equation">\xb6</a></span>'
u'<div class="math" id="equation-foo">\ne^{i\\pi} = 1</div>' in content)
assert ('<span class="eqno">(2)</span><div class="math">\n'
'e^{ix} = \\cos x + i\\sin x</div>' in content)
assert (u'<span class="eqno">(2)<a class="headerlink" href="#equation-math:0" '
u'title="Permalink to this equation">\xb6</a></span>'
u'<div class="math" id="equation-math:0">\n'
u'e^{ix} = \\cos x + i\\sin x</div>' in content)
assert '<div class="math">\nn \\in \\mathbb N</div>' in content
assert '<div class="math">\na + 1 &lt; b</div>' in content
@ -81,8 +83,8 @@ def test_math_number_all_mathjax(app, status, warning):
app.builder.build_all()
content = (app.outdir / 'index.html').text()
html = (r'<div class="math">\s*'
r'<span class="eqno">\(1\)</span>\\\[a\^2\+b\^2=c\^2\\\]</div>')
html = (r'<div class="math" id="equation-index:0">\s*'
r'<span class="eqno">\(1\)<a .*>\xb6</a></span>\\\[a\^2\+b\^2=c\^2\\\]</div>')
assert re.search(html, content, re.S)
@ -110,3 +112,6 @@ def test_math_number_all_latex(app, status, warning):
r'V &= \\frac\{4}\{3} \\pi r\^3\\\\\s*'
r'\\end{aligned}\\end{align\*}')
assert re.search(macro, content, re.S)
macro = r'Referencing equation \\eqref{equation:math:foo}.'
assert re.search(macro, content, re.S)