From bd22d09f48009a8fa1a2fb6c3a485f1d0103fcdf Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 16 Aug 2016 23:36:31 +0900 Subject: [PATCH] Add domain support to support equation cross references (ref: #1210, #2851) --- CHANGES | 2 + sphinx/ext/mathbase.py | 185 ++++++++++++++++++---------- tests/roots/test-ext-math/index.rst | 4 + tests/test_ext_math.py | 13 +- 4 files changed, 132 insertions(+), 72 deletions(-) diff --git a/CHANGES b/CHANGES index a725f5a6d..7b9b6de42 100644 --- a/CHANGES +++ b/CHANGES @@ -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 ---------- diff --git a/sphinx/ext/mathbase.py b/sphinx/ext/mathbase.py index ddcec492c..20cc3d5c4 100644 --- a/sphinx/ext/mathbase.py +++ b/sphinx/ext/mathbase.py @@ -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('' % node['target']) - - -def html_depart_eqref(self, node): - self.body.append('') - - -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) diff --git a/tests/roots/test-ext-math/index.rst b/tests/roots/test-ext-math/index.rst index 02f50c20a..9d16824f6 100644 --- a/tests/roots/test-ext-math/index.rst +++ b/tests/roots/test-ext-math/index.rst @@ -1,6 +1,10 @@ Test Math ========= +.. toctree:: + + math + .. math:: a^2+b^2=c^2 Inline :math:`E=mc^2` diff --git a/tests/test_ext_math.py b/tests/test_ext_math.py index af4b873d7..0c7d44e8e 100644 --- a/tests/test_ext_math.py +++ b/tests/test_ext_math.py @@ -25,8 +25,10 @@ def test_jsmath(app, status, warning): assert (u'(1)\xb6' u'
\ne^{i\\pi} = 1
' in content) - assert ('(2)
\n' - 'e^{ix} = \\cos x + i\\sin x
' in content) + assert (u'(2)\xb6' + u'
\n' + u'e^{ix} = \\cos x + i\\sin x
' in content) assert '
\nn \\in \\mathbb N
' in content assert '
\na + 1 < b
' 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'
\s*' - r'\(1\)\\\[a\^2\+b\^2=c\^2\\\]
') + html = (r'
\s*' + r'\(1\)\xb6\\\[a\^2\+b\^2=c\^2\\\]
') 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)