diff --git a/CHANGES b/CHANGES index 8b52dd47c..401cb164e 100644 --- a/CHANGES +++ b/CHANGES @@ -72,6 +72,11 @@ Deprecated * ``BuildEnvironment.dump()`` is deprecated * ``BuildEnvironment.dumps()`` is deprecated * ``BuildEnvironment.topickle()`` is deprecated +* ``sphinx.ext.mathbase.math`` node is deprecated +* ``sphinx.ext.mathbase.displaymath`` node is deprecated +* ``sphinx.ext.mathbase.eqref`` node is deprecated +* ``sphinx.ext.mathbase.is_in_section_title()`` is deprecated +* ``sphinx.ext.mathbase.MathDomain`` is deprecated For more details, see `deprecation APIs list `_ diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst index b089bdcea..5b9838244 100644 --- a/doc/extdev/index.rst +++ b/doc/extdev/index.rst @@ -126,6 +126,31 @@ The following is a list of deprecated interface. - 4.0 - :meth:`~sphinx.application.Sphinx.add_css_file()` + * - ``sphinx.ext.mathbase.MathDomain`` + - 1.8 + - 3.0 + - ``sphinx.domains.math.MathDomain`` + + * - ``sphinx.ext.mathbase.is_in_section_title()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.ext.mathbase.math`` (node) + - 1.8 + - 3.0 + - ``docutils.nodes.math`` + + * - ``sphinx.ext.mathbase.displaymath`` (node) + - 1.8 + - 3.0 + - ``docutils.nodes.math_block`` + + * - ``sphinx.ext.mathbase.eqref`` (node) + - 1.8 + - 3.0 + - ``sphinx.addnodes.math_reference`` + * - ``viewcode_import`` (config value) - 1.8 - 3.0 diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index e6999bd16..40390f346 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -9,8 +9,12 @@ :license: BSD, see LICENSE for details. """ +import warnings + from docutils import nodes +from sphinx.deprecation import RemovedInSphinx30Warning + if False: # For type annotation from typing import List, Sequence # NOQA @@ -183,6 +187,55 @@ class production(nodes.Part, nodes.Inline, nodes.FixedTextElement): """Node for a single grammar production rule.""" +# math nodes + + +class math(nodes.math): + """Node for inline equations. + + .. deprecated:: 1.8 + Use ``docutils.nodes.math`` instead. + """ + + def __getitem__(self, key): + """Special accessor for supporting ``node['latex']``.""" + if key == 'latex' and 'latex' not in self.attributes: + warnings.warn("math node for Sphinx was replaced by docutils'. " + "Therefore please use ``node.astext()`` to get an equation instead.", + RemovedInSphinx30Warning) + return self.astext() + else: + return nodes.math.__getitem__(self, key) + + +class math_block(nodes.math_block): + """Node for block level equations. + + .. deprecated:: 1.8 + """ + + def __getitem__(self, key): + if key == 'latex' and 'latex' not in self.attributes: + warnings.warn("displaymath node for Sphinx was replaced by docutils'. " + "Therefore please use ``node.astext()`` to get an equation instead.", + RemovedInSphinx30Warning) + return self.astext() + else: + return nodes.math_block.__getitem__(self, key) + + +class displaymath(math_block): + """Node for block level equations. + + .. deprecated:: 1.8 + """ + + +class math_reference(nodes.Inline, nodes.Referential, nodes.TextElement): + """Node for a reference for equation.""" + pass + + # other directive-level nodes class index(nodes.Invisible, nodes.Inline, nodes.TextElement): diff --git a/sphinx/application.py b/sphinx/application.py index 9d3d5de9f..782415e89 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -83,6 +83,7 @@ builtin_extensions = ( 'sphinx.domains.c', 'sphinx.domains.cpp', 'sphinx.domains.javascript', + 'sphinx.domains.math', 'sphinx.domains.python', 'sphinx.domains.rst', 'sphinx.domains.std', @@ -97,6 +98,7 @@ builtin_extensions = ( 'sphinx.roles', 'sphinx.transforms.post_transforms', 'sphinx.transforms.post_transforms.images', + 'sphinx.transforms.post_transforms.compat', 'sphinx.util.compat', # collectors should be loaded by specific order 'sphinx.environment.collectors.dependencies', diff --git a/sphinx/config.py b/sphinx/config.py index 1184f1891..4ad09c08b 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -139,6 +139,9 @@ class Config(object): numfig_secnum_depth = (1, 'env', []), numfig_format = ({}, 'env', []), # will be initialized in init_numfig_format() + math_number_all = (False, 'env', []), + math_eqref_format = (None, 'env', string_classes), + math_numfig = (True, 'env', []), tls_verify = (True, 'env', []), tls_cacerts = (None, 'env', []), smartquotes = (True, 'env', []), diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index 00be5584d..f84f082bc 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -8,6 +8,7 @@ """ from docutils import nodes +from docutils.nodes import make_id from docutils.parsers.rst import directives from docutils.parsers.rst.directives import images, html, tables @@ -105,6 +106,63 @@ class ListTable(tables.ListTable): return title, message +class MathDirective(SphinxDirective): + + has_content = True + required_arguments = 0 + optional_arguments = 1 + final_argument_whitespace = True + option_spec = { + 'label': directives.unchanged, + 'name': directives.unchanged, + 'nowrap': directives.flag, + } + + def run(self): + # type: () -> List[nodes.Node] + latex = '\n'.join(self.content) + if self.arguments and self.arguments[0]: + latex = self.arguments[0] + '\n\n' + latex + node = nodes.math_block(latex, latex, + docname=self.state.document.settings.env.docname, + number=self.options.get('name'), + label=self.options.get('label'), + nowrap='nowrap' in self.options) + ret = [node] + set_source_info(self, node) + if hasattr(self, 'src'): + node.source = self.src + self.add_target(ret) + return ret + + def add_target(self, ret): + # type: (List[nodes.Node]) -> None + node = ret[0] + + # assign label automatically if math_number_all enabled + if node['label'] == '' or (self.config.math_number_all and not node['label']): + seq = self.env.new_serialno('sphinx.ext.math#equations') + node['label'] = "%s:%d" % (self.env.docname, seq) + + # no targets and numbers are needed + if not node['label']: + return + + # register label to domain + domain = self.env.get_domain('math') + try: + eqno = domain.add_equation(self.env, self.env.docname, node['label']) # type: ignore # NOQA + node['number'] = eqno + + # add target node + node_id = make_id('equation-%s' % node['label']) + target = nodes.target('', '', ids=[node_id]) + self.state.document.note_explicit_target(target) + ret.insert(0, target) + except UserWarning as exc: + self.state_machine.reporter.warning(exc.args[0], line=self.lineno) + + def setup(app): # type: (Sphinx) -> Dict directives.register_directive('figure', Figure) @@ -112,6 +170,7 @@ def setup(app): directives.register_directive('table', RSTTable) directives.register_directive('csv-table', CSVTable) directives.register_directive('list-table', ListTable) + directives.register_directive('math', MathDirective) return { 'version': 'builtin', diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py new file mode 100644 index 000000000..a5de8a892 --- /dev/null +++ b/sphinx/domains/math.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" + sphinx.domains.math + ~~~~~~~~~~~~~~~~~~~ + + The math domain. + + :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from docutils import nodes +from docutils.nodes import make_id + +from sphinx.addnodes import math_block as displaymath, math_reference +from sphinx.domains import Domain +from sphinx.locale import __ +from sphinx.roles import XRefRole +from sphinx.util import logging +from sphinx.util.nodes import make_refnode + +if False: + # For type annotation + from typing import Any, Callable, Dict, Iterable, List, Tuple # NOQA + from sphinx.application import Sphinx # NOQA + from sphinx.builders import Builder # NOQA + from sphinx.environment import BuildEnvironment # NOQA + +logger = logging.getLogger(__name__) + + +class MathReferenceRole(XRefRole): + def result_nodes(self, document, env, node, is_ref): + # type: (nodes.Node, BuildEnvironment, nodes.Node, bool) -> Tuple[List[nodes.Node], List[nodes.Node]] # NOQA + node['refdomain'] = 'math' + return [node], [] + + +class MathDomain(Domain): + """Mathematics domain.""" + name = 'math' + label = 'mathematics' + + initial_data = { + 'objects': {}, # labelid -> (docname, eqno) + } # type: Dict[unicode, Dict[unicode, Tuple[unicode, int]]] + dangling_warnings = { + 'eq': 'equation not found: %(target)s', + } + enumerable_nodes = { # node_class -> (figtype, title_getter) + displaymath: ('displaymath', None), + nodes.math_block: ('displaymath', None), + } # type: Dict[nodes.Node, Tuple[unicode, Callable]] + + def clear_doc(self, docname): + # type: (unicode) -> None + for equation_id, (doc, eqno) in list(self.data['objects'].items()): + if doc == docname: + del self.data['objects'][equation_id] + + def merge_domaindata(self, docnames, otherdata): + # type: (Iterable[unicode], Dict) -> None + for labelid, (doc, eqno) in otherdata['objects'].items(): + if doc in docnames: + self.data['objects'][labelid] = (doc, eqno) + + def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): + # type: (BuildEnvironment, unicode, Builder, unicode, unicode, nodes.Node, nodes.Node) -> nodes.Node # NOQA + assert typ == 'eq' + docname, number = self.data['objects'].get(target, (None, None)) + if docname: + if builder.name == 'latex': + newnode = math_reference('', **node.attributes) + newnode['docname'] = docname + newnode['target'] = target + return newnode + else: + # TODO: perhaps use rather a sphinx-core provided prefix here? + node_id = make_id('equation-%s' % target) + if env.config.math_numfig and env.config.numfig: + if docname in env.toc_fignumbers: + number = env.toc_fignumbers[docname]['displaymath'].get(node_id, ()) + number = '.'.join(map(str, number)) + else: + number = '' + try: + eqref_format = env.config.math_eqref_format or "({number})" + title = nodes.Text(eqref_format.format(number=number)) + except KeyError as exc: + logger.warning(__('Invalid math_eqref_format: %r'), exc, + location=node) + title = nodes.Text("(%d)" % number) + title = nodes.Text("(%d)" % number) + return make_refnode(builder, fromdocname, docname, node_id, title) + else: + return None + + def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): + # type: (BuildEnvironment, unicode, Builder, unicode, nodes.Node, nodes.Node) -> List[nodes.Node] # NOQA + refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode) + if refnode is None: + return [] + else: + return [refnode] + + def get_objects(self): + # type: () -> List + return [] + + def add_equation(self, env, docname, labelid): + # type: (BuildEnvironment, unicode, unicode) -> int + 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): + # type: (unicode) -> int + targets = [eq for eq in self.data['objects'].values() if eq[0] == docname] + return len(targets) + 1 + + +def setup(app): + # type: (Sphinx) -> Dict[unicode, Any] + app.add_domain(MathDomain) + app.add_role('eq', MathReferenceRole(warn_dangling=True)) + + return { + 'version': 'builtin', + 'env_version': 1, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index a1faf2c1b..0cdc9aba4 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -37,7 +37,7 @@ if False: from sphinx.application import Sphinx # NOQA from sphinx.builders import Builder # NOQA from sphinx.config import Config # NOQA - from sphinx.ext.mathbase import math as math_node, displaymath # NOQA + from sphinx.ext.mathbase import displaymath # NOQA logger = logging.getLogger(__name__) @@ -285,27 +285,27 @@ def cleanup_tempdir(app, exc): def get_tooltip(self, node): - # type: (nodes.NodeVisitor, math_node) -> unicode + # type: (nodes.NodeVisitor, nodes.math) -> unicode if self.builder.config.imgmath_add_tooltips: - return ' alt="%s"' % self.encode(node['latex']).strip() + return ' alt="%s"' % self.encode(node.astext()).strip() return '' def html_visit_math(self, node): - # type: (nodes.NodeVisitor, math_node) -> None + # type: (nodes.NodeVisitor, nodes.math) -> None try: - fname, depth = render_math(self, '$' + node['latex'] + '$') + fname, depth = render_math(self, '$' + node.astext() + '$') except MathExtError as exc: msg = text_type(exc) sm = nodes.system_message(msg, type='WARNING', level=2, - backrefs=[], source=node['latex']) + backrefs=[], source=node.astext()) sm.walkabout(self) - logger.warning(__('display latex %r: %s'), node['latex'], msg) + logger.warning(__('display latex %r: %s'), node.astext(), msg) raise nodes.SkipNode if fname is None: # something failed -- use text-only as a bad substitute self.body.append('%s' % - self.encode(node['latex']).strip()) + self.encode(node.astext()).strip()) else: c = (' None if node['nowrap']: - latex = node['latex'] + latex = node.astext() else: - latex = wrap_displaymath(node['latex'], None, + latex = wrap_displaymath(node.astext(), None, self.builder.config.math_number_all) try: fname, depth = render_math(self, latex) except MathExtError as exc: msg = text_type(exc) sm = nodes.system_message(msg, type='WARNING', level=2, - backrefs=[], source=node['latex']) + backrefs=[], source=node.astext()) sm.walkabout(self) - logger.warning(__('inline latex %r: %s'), node['latex'], msg) + logger.warning(__('inline latex %r: %s'), node.astext(), msg) raise nodes.SkipNode self.body.append(self.starttag(node, 'div', CLASS='math')) self.body.append('

') @@ -340,7 +340,7 @@ def html_visit_displaymath(self, node): if fname is None: # something failed -- use text-only as a bad substitute self.body.append('%s

\n' % - self.encode(node['latex']).strip()) + self.encode(node.astext()).strip()) else: self.body.append(('

\n') diff --git a/sphinx/ext/jsmath.py b/sphinx/ext/jsmath.py index 97ea400a3..7381da42b 100644 --- a/sphinx/ext/jsmath.py +++ b/sphinx/ext/jsmath.py @@ -27,7 +27,7 @@ if False: def html_visit_math(self, node): # type: (nodes.NodeVisitor, nodes.Node) -> None self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight')) - self.body.append(self.encode(node['latex']) + '') + self.body.append(self.encode(node.astext()) + '') raise nodes.SkipNode @@ -35,10 +35,10 @@ def html_visit_displaymath(self, node): # type: (nodes.NodeVisitor, nodes.Node) -> None if node['nowrap']: self.body.append(self.starttag(node, 'div', CLASS='math notranslate nohighlight')) - self.body.append(self.encode(node['latex'])) + self.body.append(self.encode(node.astext())) self.body.append('') raise nodes.SkipNode - for i, part in enumerate(node['latex'].split('\n\n')): + for i, part in enumerate(node.astext().split('\n\n')): part = self.encode(part) if i == 0: # necessary to e.g. set the id property correctly diff --git a/sphinx/ext/mathbase.py b/sphinx/ext/mathbase.py index e6a6929e6..448f329b2 100644 --- a/sphinx/ext/mathbase.py +++ b/sphinx/ext/mathbase.py @@ -9,135 +9,21 @@ :license: BSD, see LICENSE for details. """ -from docutils import nodes, utils -from docutils.nodes import make_id -from docutils.parsers.rst import directives +import warnings -from sphinx.config import string_classes -from sphinx.domains import Domain -from sphinx.locale import __ -from sphinx.roles import XRefRole -from sphinx.util import logging -from sphinx.util.docutils import SphinxDirective -from sphinx.util.nodes import make_refnode, set_source_info +from docutils import nodes + +from sphinx.addnodes import math, math_block as displaymath +from sphinx.addnodes import math_reference as eqref # NOQA # to keep compatibility +from sphinx.deprecation import RemovedInSphinx30Warning +from sphinx.domains.math import MathDomain # NOQA # to keep compatibility +from sphinx.domains.math import MathReferenceRole as EqXRefRole # NOQA # to keep compatibility if False: # For type annotation - from typing import Any, Callable, Dict, Iterable, List, Tuple # NOQA - from docutils.parsers.rst.states import Inliner # NOQA + from typing import Any, Callable, List, Tuple # NOQA from docutils.writers.html4css1 import Writer # NOQA from sphinx.application import Sphinx # NOQA - from sphinx.builders import Builder # NOQA - from sphinx.environment import BuildEnvironment # NOQA - -logger = logging.getLogger(__name__) - - -class math(nodes.Inline, nodes.TextElement): - pass - - -class displaymath(nodes.Part, nodes.Element): - pass - - -class eqref(nodes.Inline, nodes.TextElement): - pass - - -class EqXRefRole(XRefRole): - def result_nodes(self, document, env, node, is_ref): - # type: (nodes.Node, BuildEnvironment, nodes.Node, bool) -> Tuple[List[nodes.Node], List[nodes.Node]] # NOQA - node['refdomain'] = 'math' - return [node], [] - - -class MathDomain(Domain): - """Mathematics domain.""" - name = 'math' - label = 'mathematics' - - initial_data = { - 'objects': {}, # labelid -> (docname, eqno) - } # type: Dict[unicode, Dict[unicode, Tuple[unicode, int]]] - dangling_warnings = { - 'eq': 'equation not found: %(target)s', - } - enumerable_nodes = { # node_class -> (figtype, title_getter) - displaymath: ('displaymath', None), - } # type: Dict[nodes.Node, Tuple[unicode, Callable]] - - def clear_doc(self, docname): - # type: (unicode) -> None - for equation_id, (doc, eqno) in list(self.data['objects'].items()): - if doc == docname: - del self.data['objects'][equation_id] - - def merge_domaindata(self, docnames, otherdata): - # type: (Iterable[unicode], Dict) -> None - for labelid, (doc, eqno) in otherdata['objects'].items(): - if doc in docnames: - self.data['objects'][labelid] = (doc, eqno) - - def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): - # type: (BuildEnvironment, unicode, Builder, unicode, unicode, nodes.Node, nodes.Node) -> nodes.Node # NOQA - 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: - # TODO: perhaps use rather a sphinx-core provided prefix here? - node_id = make_id('equation-%s' % target) - if env.config.math_numfig and env.config.numfig: - if docname in env.toc_fignumbers: - number = env.toc_fignumbers[docname]['displaymath'].get(node_id, ()) - number = '.'.join(map(str, number)) - else: - number = '' - try: - eqref_format = env.config.math_eqref_format or "({number})" - title = nodes.Text(eqref_format.format(number=number)) - except KeyError as exc: - logger.warning(__('Invalid math_eqref_format: %r'), exc, - location=node) - title = nodes.Text("(%d)" % number) - title = nodes.Text("(%d)" % number) - return make_refnode(builder, fromdocname, docname, node_id, title) - else: - return None - - def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): - # type: (BuildEnvironment, unicode, Builder, unicode, nodes.Node, nodes.Node) -> List[nodes.Node] # NOQA - refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode) - if refnode is None: - return [] - else: - return [refnode] - - def get_objects(self): - # type: () -> List - return [] - - def add_equation(self, env, docname, labelid): - # type: (BuildEnvironment, unicode, unicode) -> int - 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): - # type: (unicode) -> int - targets = [eq for eq in self.data['objects'].values() if eq[0] == docname] - return len(targets) + 1 def get_node_equation_number(writer, node): @@ -195,17 +81,14 @@ def wrap_displaymath(math, label, numbering): return '%s\n%s%s' % (begin, ''.join(equations), end) -def math_role(role, rawtext, text, lineno, inliner, options={}, content=[]): - # type: (unicode, unicode, unicode, int, Inliner, Dict, List[unicode]) -> Tuple[List[nodes.Node], List[nodes.Node]] # NOQA - latex = utils.unescape(text, restore_backslashes=True) - return [math(latex=latex)], [] - - def is_in_section_title(node): # type: (nodes.Node) -> bool """Determine whether the node is in a section title""" from sphinx.util.nodes import traverse_parent + warnings.warn('is_in_section_title() is deprecated.', + RemovedInSphinx30Warning) + for ancestor in traverse_parent(node): if isinstance(ancestor, nodes.title) and \ isinstance(ancestor.parent, nodes.section): @@ -213,181 +96,9 @@ def is_in_section_title(node): return False -class MathDirective(SphinxDirective): - - has_content = True - required_arguments = 0 - optional_arguments = 1 - final_argument_whitespace = True - option_spec = { - 'label': directives.unchanged, - 'name': directives.unchanged, - 'nowrap': directives.flag, - } - - def run(self): - # type: () -> List[nodes.Node] - latex = '\n'.join(self.content) - if self.arguments and self.arguments[0]: - latex = self.arguments[0] + '\n\n' + latex - node = displaymath() - node['latex'] = latex - 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.env.docname - ret = [node] - set_source_info(self, node) - if hasattr(self, 'src'): - node.source = self.src - self.add_target(ret) - return ret - - def add_target(self, ret): - # type: (List[nodes.Node]) -> None - node = ret[0] - - # assign label automatically if math_number_all enabled - if node['label'] == '' or (self.config.math_number_all and not node['label']): - seq = self.env.new_serialno('sphinx.ext.math#equations') - node['label'] = "%s:%d" % (self.env.docname, seq) - - # no targets and numbers are needed - if not node['label']: - return - - # register label to domain - domain = self.env.get_domain('math') - try: - eqno = domain.add_equation(self.env, self.env.docname, node['label']) # type: ignore # NOQA - node['number'] = eqno - - # add target node - node_id = make_id('equation-%s' % node['label']) - target = nodes.target('', '', ids=[node_id]) - self.state.document.note_explicit_target(target) - ret.insert(0, target) - except UserWarning as exc: - self.state_machine.reporter.warning(exc.args[0], line=self.lineno) - - -def latex_visit_math(self, node): - # type: (nodes.NodeVisitor, math) -> None - if is_in_section_title(node): - protect = r'\protect' - else: - protect = '' - equation = protect + r'\(' + node['latex'] + protect + r'\)' - self.body.append(equation) - raise nodes.SkipNode - - -def latex_visit_displaymath(self, node): - # type: (nodes.NodeVisitor, displaymath) -> None - if not node['label']: - label = None - else: - label = "equation:%s:%s" % (node['docname'], node['label']) - - if node['nowrap']: - if label: - self.body.append(r'\label{%s}' % label) - self.body.append(node['latex']) - else: - self.body.append(wrap_displaymath(node['latex'], label, - self.builder.config.math_number_all)) - raise nodes.SkipNode - - -def latex_visit_eqref(self, node): - # type: (nodes.NodeVisitor, eqref) -> None - label = "equation:%s:%s" % (node['docname'], node['target']) - eqref_format = self.builder.config.math_eqref_format - if eqref_format: - try: - ref = '\\ref{%s}' % label - self.body.append(eqref_format.format(number=ref)) - except KeyError as exc: - logger.warning(__('Invalid math_eqref_format: %r'), exc, - location=node) - self.body.append('\\eqref{%s}' % label) - else: - self.body.append('\\eqref{%s}' % label) - raise nodes.SkipNode - - -def text_visit_math(self, node): - # type: (nodes.NodeVisitor, math) -> None - self.add_text(node['latex']) - raise nodes.SkipNode - - -def text_visit_displaymath(self, node): - # type: (nodes.NodeVisitor, displaymath) -> None - self.new_state() - self.add_text(node['latex']) - self.end_state() - raise nodes.SkipNode - - -def man_visit_math(self, node): - # type: (nodes.NodeVisitor, math) -> None - self.body.append(node['latex']) - raise nodes.SkipNode - - -def man_visit_displaymath(self, node): - # type: (nodes.NodeVisitor, displaymath) -> None - self.visit_centered(node) - - -def man_depart_displaymath(self, node): - # type: (nodes.NodeVisitor, displaymath) -> None - self.depart_centered(node) - - -def texinfo_visit_math(self, node): - # type: (nodes.NodeVisitor, math) -> None - self.body.append('@math{' + self.escape_arg(node['latex']) + '}') - raise nodes.SkipNode - - -def texinfo_visit_displaymath(self, node): - # type: (nodes.NodeVisitor, displaymath) -> None - if node.get('label'): - self.add_anchor(node['label'], node) - self.body.append('\n\n@example\n%s\n@end example\n\n' % - self.escape_arg(node['latex'])) - - -def texinfo_depart_displaymath(self, node): - # type: (nodes.NodeVisitor, displaymath) -> None - pass - - def setup_math(app, htmlinlinevisitors, htmldisplayvisitors): # type: (Sphinx, Tuple[Callable, Any], Tuple[Callable, Any]) -> None - app.add_config_value('math_number_all', False, 'env') - app.add_config_value('math_eqref_format', None, 'env', string_classes) - app.add_config_value('math_numfig', True, 'env') - app.add_domain(MathDomain) app.add_node(math, override=True, - latex=(latex_visit_math, None), - text=(text_visit_math, None), - man=(man_visit_math, None), - texinfo=(texinfo_visit_math, None), html=htmlinlinevisitors) - app.add_node(displaymath, - latex=(latex_visit_displaymath, None), - text=(text_visit_displaymath, None), - man=(man_visit_displaymath, man_depart_displaymath), - texinfo=(texinfo_visit_displaymath, texinfo_depart_displaymath), + app.add_node(displaymath, override=True, html=htmldisplayvisitors) - app.add_node(eqref, latex=(latex_visit_eqref, None)) - app.add_role('math', math_role) - app.add_role('eq', EqXRefRole(warn_dangling=True)) - app.add_directive('math', MathDirective) diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py index 2bb7eec09..39fbb8539 100644 --- a/sphinx/ext/mathjax.py +++ b/sphinx/ext/mathjax.py @@ -29,7 +29,7 @@ def html_visit_math(self, node): # type: (nodes.NodeVisitor, nodes.Node) -> None self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight')) self.body.append(self.builder.config.mathjax_inline[0] + - self.encode(node['latex']) + + self.encode(node.astext()) + self.builder.config.mathjax_inline[1] + '') raise nodes.SkipNode @@ -38,7 +38,7 @@ def html_visit_displaymath(self, node): # type: (nodes.NodeVisitor, nodes.Node) -> None self.body.append(self.starttag(node, 'div', CLASS='math notranslate nohighlight')) if node['nowrap']: - self.body.append(self.encode(node['latex'])) + self.body.append(self.encode(node.astext())) self.body.append('') raise nodes.SkipNode @@ -49,7 +49,7 @@ def html_visit_displaymath(self, node): self.add_permalink_ref(node, _('Permalink to this equation')) self.body.append('') self.body.append(self.builder.config.mathjax_display[0]) - parts = [prt for prt in node['latex'].split('\n\n') if prt.strip()] + parts = [prt for prt in node.astext().split('\n\n') if prt.strip()] if len(parts) > 1: # Add alignment if there are more than 1 equation self.body.append(r' \begin{align}\begin{aligned}') for i, part in enumerate(parts): diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py index 76ed154fd..94ac9a6e7 100644 --- a/sphinx/testing/fixtures.py +++ b/sphinx/testing/fixtures.py @@ -163,7 +163,7 @@ def make_app(test_params, monkeypatch): yield make sys.path[:] = syspath - for app_ in apps: + for app_ in reversed(apps): # clean up applications from the new ones app_.cleanup() diff --git a/sphinx/transforms/post_transforms/compat.py b/sphinx/transforms/post_transforms/compat.py new file mode 100644 index 000000000..94360f038 --- /dev/null +++ b/sphinx/transforms/post_transforms/compat.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" + sphinx.transforms.post_transforms.compat + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Post transforms for compatibility + + :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import warnings +from typing import TYPE_CHECKING + +from docutils import nodes +from docutils.writers.docutils_xml import XMLTranslator + +from sphinx.addnodes import math_block, displaymath +from sphinx.deprecation import RemovedInSphinx30Warning +from sphinx.transforms import SphinxTransform +from sphinx.util import logging + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Iterable, List, Tuple # NOQA + from docutils.parsers.rst.states import Inliner # NOQA + from docutils.writers.html4css1 import Writer # NOQA + from sphinx.application import Sphinx # NOQA + from sphinx.builders import Builder # NOQA + from sphinx.environment import BuildEnvironment # NOQA + +logger = logging.getLogger(__name__) + + +class MathNodeMigrator(SphinxTransform): + """Migrate a math node to docutils'. + + For a long time, Sphinx uses an original node for math. Since 1.8, + Sphinx starts to use a math node of docutils'. This transform converts + old and new nodes to keep compatibility. + """ + default_priority = 999 + + def apply(self): + # type: () -> None + for node in self.document.traverse(nodes.math): + # case: old styled ``math`` node generated by old extensions + if len(node) == 0: + warnings.warn("math node for Sphinx was replaced by docutils'. " + "Please use ``docutils.nodes.math`` instead.", + RemovedInSphinx30Warning) + equation = node['latex'] + node += nodes.Text(equation, equation) + + translator = self.app.builder.get_translator_class() + if hasattr(translator, 'visit_displaymath') and translator != XMLTranslator: + # case: old translators which does not support ``math_block`` node + warnings.warn("Translator for %s does not support math_block node'. " + "Please update your extension." % translator, + RemovedInSphinx30Warning) + for node in self.document.traverse(math_block): + alt = displaymath(latex=node.astext(), + number=node.get('number'), + label=node.get('label'), + nowrap=node.get('nowrap'), + docname=node.get('docname')) + node.replace(alt) + else: + # case: old styled ``displaymath`` node generated by old extensions + for node in self.document.traverse(math_block): + if len(node) == 0: + warnings.warn("math node for Sphinx was replaced by docutils'. " + "Please use ``docutils.nodes.math_block`` instead.", + RemovedInSphinx30Warning) + latex = node['latex'] + node += nodes.Text(latex, latex) + + +def setup(app): + # type: (Sphinx) -> Dict[unicode, Any] + app.add_post_transform(MathNodeMigrator) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 2c0cc0424..0ccc46e4b 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -2542,13 +2542,51 @@ class LaTeXTranslator(nodes.NodeVisitor): def visit_math(self, node): # type: (nodes.Node) -> None - logger.warning(__('using "math" markup without a Sphinx math extension ' - 'active, please use one of the math extensions ' - 'described at http://sphinx-doc.org/en/master/ext/math.html'), - location=(self.curfilestack[-1], node.line)) + if self.in_title: + self.body.append(r'\protect\(%s\protect\)' % node.astext()) + else: + self.body.append(r'\(%s\)' % node.astext()) raise nodes.SkipNode - visit_math_block = visit_math + def visit_math_block(self, node): + # type: (nodes.Node) -> None + if node.get('label'): + label = "equation:%s:%s" % (node['docname'], node['label']) + else: + label = None + + if node.get('nowrap'): + if label: + self.body.append(r'\label{%s}' % label) + self.body.append(node.astext()) + else: + def is_equation(part): + # type: (unicode) -> unicode + return part.strip() + + from sphinx.ext.mathbase import wrap_displaymath + self.body.append(wrap_displaymath(node.astext(), label, + self.builder.config.math_number_all)) + raise nodes.SkipNode + + def visit_math_reference(self, node): + # type: (nodes.Node) -> None + label = "equation:%s:%s" % (node['docname'], node['target']) + eqref_format = self.builder.config.math_eqref_format + if eqref_format: + try: + ref = r'\ref{%s}' % label + self.body.append(eqref_format.format(number=ref)) + except KeyError as exc: + logger.warning(__('Invalid math_eqref_format: %r'), exc, + location=node) + self.body.append(r'\eqref{%s}' % label) + else: + self.body.append(r'\eqref{%s}' % label) + + def depart_math_reference(self, node): + # type: (nodes.Node) -> None + pass def unknown_visit(self, node): # type: (nodes.Node) -> None diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index c6c8723dd..02540a53d 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -18,7 +18,7 @@ from docutils.writers.manpage import ( import sphinx.util.docutils from sphinx import addnodes -from sphinx.locale import admonitionlabels, _, __ +from sphinx.locale import admonitionlabels, _ from sphinx.util import logging from sphinx.util.i18n import format_date @@ -513,12 +513,19 @@ class ManualPageTranslator(BaseTranslator): def visit_math(self, node): # type: (nodes.Node) -> None - logger.warning(__('using "math" markup without a Sphinx math extension ' - 'active, please use one of the math extensions ' - 'described at http://sphinx-doc.org/en/master/ext/math.html')) - raise nodes.SkipNode + pass - visit_math_block = visit_math + def depart_math(self, node): + # type: (nodes.Node) -> None + pass + + def visit_math_block(self, node): + # type: (nodes.Node) -> None + self.visit_centered(node) + + def depart_math_block(self, node): + # type: (nodes.Node) -> None + self.depart_centered(node) def unknown_visit(self, node): # type: (nodes.Node) -> None diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index e58d659ab..4f76c6cf9 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -1731,9 +1731,13 @@ class TexinfoTranslator(nodes.NodeVisitor): def visit_math(self, node): # type: (nodes.Node) -> None - logger.warning(__('using "math" markup without a Sphinx math extension ' - 'active, please use one of the math extensions ' - 'described at http://sphinx-doc.org/en/master/ext/math.html')) + self.body.append('@math{' + self.escape_arg(node.astext()) + '}') raise nodes.SkipNode - visit_math_block = visit_math + def visit_math_block(self, node): + # type: (nodes.Node) -> None + if node.get('label'): + self.add_anchor(node['label'], node) + self.body.append('\n\n@example\n%s\n@end example\n\n' % + self.escape_arg(node.astext())) + raise nodes.SkipNode diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 379f06b46..0f9b45fbe 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -18,7 +18,7 @@ from docutils.utils import column_width from six.moves import zip_longest from sphinx import addnodes -from sphinx.locale import admonitionlabels, _, __ +from sphinx.locale import admonitionlabels, _ from sphinx.util import logging if False: @@ -1179,13 +1179,19 @@ class TextTranslator(nodes.NodeVisitor): def visit_math(self, node): # type: (nodes.Node) -> None - logger.warning(__('using "math" markup without a Sphinx math extension ' - 'active, please use one of the math extensions ' - 'described at http://sphinx-doc.org/en/master/ext/math.html'), - location=(self.builder.current_docname, node.line)) - raise nodes.SkipNode + pass - visit_math_block = visit_math + def depart_math(self, node): + # type: (nodes.Node) -> None + pass + + def visit_math_block(self, node): + # type: (nodes.Node) -> None + self.new_state() + + def depart_math_block(self, node): + # type: (nodes.Node) -> None + self.end_state() def unknown_visit(self, node): # type: (nodes.Node) -> None diff --git a/tests/roots/test-ext-math-compat/conf.py b/tests/roots/test-ext-math-compat/conf.py new file mode 100644 index 000000000..c4f6005ea --- /dev/null +++ b/tests/roots/test-ext-math-compat/conf.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +from docutils.parsers.rst import Directive + +from sphinx.ext.mathbase import math, displaymath + +master_doc = 'index' +extensions = ['sphinx.ext.mathjax'] + + +def my_math_role(role, rawtext, text, lineno, inliner, options={}, content=[]): + return [math(latex='E = mc^2')], [] + + +class MyMathDirective(Directive): + def run(self): + return [displaymath(latex='E = mc^2')] + + +def setup(app): + app.add_role('my_math', my_math_role) + app.add_directive('my-math', MyMathDirective) diff --git a/tests/roots/test-ext-math-compat/index.rst b/tests/roots/test-ext-math-compat/index.rst new file mode 100644 index 000000000..208878c36 --- /dev/null +++ b/tests/roots/test-ext-math-compat/index.rst @@ -0,0 +1,21 @@ +Test Math +========= + +inline +------ + +Inline: :math:`E=mc^2` +Inline my math: :my_math:`:-)` + +block +----- + +.. math:: a^2+b^2=c^2 + +Second math + +.. math:: e^{i\pi}+1=0 + +Multi math equations + +.. my-math:: diff --git a/tests/test_config.py b/tests/test_config.py index e3b79c835..6a910ecfc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -173,25 +173,19 @@ def test_needs_sphinx(make_app_with_empty_project): make_app = make_app_with_empty_project # micro version app = make_app(confoverrides={'needs_sphinx': '1.3.3'}) # OK: less - app.cleanup() app = make_app(confoverrides={'needs_sphinx': '1.3.4'}) # OK: equals - app.cleanup() with pytest.raises(VersionRequirementError): make_app(confoverrides={'needs_sphinx': '1.3.5'}) # NG: greater # minor version app = make_app(confoverrides={'needs_sphinx': '1.2'}) # OK: less - app.cleanup() app = make_app(confoverrides={'needs_sphinx': '1.3'}) # OK: equals - app.cleanup() with pytest.raises(VersionRequirementError): make_app(confoverrides={'needs_sphinx': '1.4'}) # NG: greater # major version app = make_app(confoverrides={'needs_sphinx': '0'}) # OK: less - app.cleanup() app = make_app(confoverrides={'needs_sphinx': '1'}) # OK: equals - app.cleanup() with pytest.raises(VersionRequirementError): make_app(confoverrides={'needs_sphinx': '2'}) # NG: greater diff --git a/tests/test_ext_math.py b/tests/test_ext_math.py index 28ce094a8..47465f07e 100644 --- a/tests/test_ext_math.py +++ b/tests/test_ext_math.py @@ -12,8 +12,12 @@ import errno import re import subprocess +import warnings import pytest +from docutils import nodes + +from sphinx.testing.util import assert_node def has_binary(binary): @@ -208,3 +212,28 @@ def test_imgmath_numfig_html(app, status, warning): 'href="math.html#equation-foo">(1) and ' '(3).

') assert html in content + + +@pytest.mark.sphinx('dummy', testroot='ext-math-compat') +def test_math_compat(app, status, warning): + with warnings.catch_warnings(record=True): + app.builder.build_all() + doctree = app.env.get_and_resolve_doctree('index', app.builder) + + assert_node(doctree, + [nodes.document, nodes.section, (nodes.title, + [nodes.section, (nodes.title, + nodes.paragraph)], + nodes.section)]) + assert_node(doctree[0][1][1], + ('Inline: ', + [nodes.math, "E=mc^2"], + '\nInline my math: ', + [nodes.math, "E = mc^2"])) + assert_node(doctree[0][2], + ([nodes.title, "block"], + [nodes.math_block, "a^2+b^2=c^2\n\n"], + [nodes.paragraph, "Second math"], + [nodes.math_block, "e^{i\\pi}+1=0\n\n"], + [nodes.paragraph, "Multi math equations"], + [nodes.math_block, "E = mc^2"]))