Repository: sphinx-doc/sphinx

Issue:
Do not include MathJax.js unless it is really needed
When a project does not have any math directives or roles, still every HTML page has this tag when built with Sphinx 1.8:
```html
<script async="async" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
```

This is unwanted, and did not happen with Sphinx 1.7.

It would be nice if the tag was included only when the page (or the project as a whole) has a math directive or role.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536?src=pr&el=h1) Report
> Merging [#5536](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536?src=pr&el=desc) into [1.8](https://codecov.io/gh/sphinx-doc/sphinx/commit/be2b86cc15a2f5fca1581c25ebf2db9a189dbc41?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `82.75%`.

[![Impacted file tree graph](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536/graphs/tree.svg?width=650&token=VFlFSELbkw&height=150&src=pr)](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##              1.8    #5536      +/-   ##
==========================================
+ Coverage   82.04%   82.05%   +<.01%     
==========================================
  Files         306      306              
  Lines       40367    40385      +18     
  Branches     6240     6241       +1     
==========================================
+ Hits        33121    33136      +15     
- Misses       5861     5864       +3     
  Partials     1385     1385
```


| [Impacted Files](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/test\_ext\_math.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9leHRfbWF0aC5weQ==) | `84.42% <100%> (+1.09%)` | :arrow_up: |
| [sphinx/ext/mathjax.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536/diff?src=pr&el=tree#diff-c3BoaW54L2V4dC9tYXRoamF4LnB5) | `89.06% <66.66%> (-1.42%)` | :arrow_down: |
| [sphinx/domains/math.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536/diff?src=pr&el=tree#diff-c3BoaW54L2RvbWFpbnMvbWF0aC5weQ==) | `74.41% <77.77%> (+0.39%)` | :arrow_up: |
| [sphinx/ext/jsmath.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536/diff?src=pr&el=tree#diff-c3BoaW54L2V4dC9qc21hdGgucHk=) | `91.3% <80%> (ø)` | :arrow_up: |
| [sphinx/util/logging.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536/diff?src=pr&el=tree#diff-c3BoaW54L3V0aWwvbG9nZ2luZy5weQ==) | `93.97% <0%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536?src=pr&el=footer). Last update [be2b86c...410f94f](https://codecov.io/gh/sphinx-doc/sphinx/pull/5536?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGES b/CHANGES
index 761ed2539..05e4ef2e0 100644
--- a/CHANGES
+++ b/CHANGES
@@ -7,6 +7,8 @@ Dependencies
 Incompatible changes
 --------------------
 
+* #5497: Do not include MathJax.js and jsmath.js unless it is really needed
+
 Deprecated
 ----------
 
diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py
index c88481746..d56f45e0e 100644
--- a/sphinx/domains/math.py
+++ b/sphinx/domains/math.py
@@ -44,6 +44,7 @@ class MathDomain(Domain):
 
     initial_data = {
         'objects': {},  # labelid -> (docname, eqno)
+        'has_equations': {},  # docname -> bool
     }  # type: Dict[unicode, Dict[unicode, Tuple[unicode, int]]]
     dangling_warnings = {
         'eq': 'equation not found: %(target)s',
@@ -56,18 +57,30 @@ class MathDomain(Domain):
         'numref': MathReferenceRole(),
     }
 
+    def process_doc(self, env, docname, document):
+        # type: (BuildEnvironment, unicode, nodes.Node) -> None
+        def math_node(node):
+            return isinstance(node, (nodes.math, nodes.math_block))
+
+        self.data['has_equations'][docname] = any(document.traverse(math_node))
+
     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]
 
+        self.data['has_equations'].pop(docname, None)
+
     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)
 
+        for docname in docnames:
+            self.data['has_equations'][docname] = otherdata['has_equations'][docname]
+
     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 in ('eq', 'numref')
@@ -122,6 +135,10 @@ 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 has_equations(self):
+        # type: () -> bool
+        return any(self.data['has_equations'].values())
+
 
 def setup(app):
     # type: (Sphinx) -> Dict[unicode, Any]
@@ -130,7 +147,7 @@ def setup(app):
 
     return {
         'version': 'builtin',
-        'env_version': 1,
+        'env_version': 2,
         'parallel_read_safe': True,
         'parallel_write_safe': True,
     }
diff --git a/sphinx/ext/jsmath.py b/sphinx/ext/jsmath.py
index 0ee42050c..3babd408e 100644
--- a/sphinx/ext/jsmath.py
+++ b/sphinx/ext/jsmath.py
@@ -21,6 +21,7 @@
     # For type annotation
     from typing import Any, Dict  # NOQA
     from sphinx.application import Sphinx  # NOQA
+    from sphinx.environment import BuildEnvironment  # NOQA
 
 
 def html_visit_math(self, node):
@@ -58,14 +59,16 @@ def html_visit_displaymath(self, node):
     raise nodes.SkipNode
 
 
-def builder_inited(app):
-    # type: (Sphinx) -> None
+def install_jsmath(app, env):
+    # type: (Sphinx, BuildEnvironment) -> None
     if app.builder.format != 'html' or app.builder.math_renderer_name != 'jsmath':  # type: ignore  # NOQA
-        pass
-    elif not app.config.jsmath_path:
+        return
+    if not app.config.jsmath_path:
         raise ExtensionError('jsmath_path config value must be set for the '
                              'jsmath extension to work')
-    if app.builder.format == 'html':
+
+    if env.get_domain('math').has_equations():  # type: ignore
+        # Enable jsmath only if equations exists
         app.builder.add_js_file(app.config.jsmath_path)  # type: ignore
 
 
@@ -76,5 +79,5 @@ def setup(app):
                                (html_visit_displaymath, None))
 
     app.add_config_value('jsmath_path', '', False)
-    app.connect('builder-inited', builder_inited)
+    app.connect('env-check-consistency', install_jsmath)
     return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py
index 50a2bae4d..21406c451 100644
--- a/sphinx/ext/mathjax.py
+++ b/sphinx/ext/mathjax.py
@@ -24,6 +24,7 @@
     # For type annotation
     from typing import Any, Dict  # NOQA
     from sphinx.application import Sphinx  # NOQA
+    from sphinx.environment import BuildEnvironment  # NOQA
 
 
 def html_visit_math(self, node):
@@ -68,14 +69,16 @@ def html_visit_displaymath(self, node):
     raise nodes.SkipNode
 
 
-def builder_inited(app):
-    # type: (Sphinx) -> None
+def install_mathjax(app, env):
+    # type: (Sphinx, BuildEnvironment) -> None
     if app.builder.format != 'html' or app.builder.math_renderer_name != 'mathjax':  # type: ignore  # NOQA
-        pass
-    elif not app.config.mathjax_path:
+        return
+    if not app.config.mathjax_path:
         raise ExtensionError('mathjax_path config value must be set for the '
                              'mathjax extension to work')
-    if app.builder.format == 'html':
+
+    if env.get_domain('math').has_equations():  # type: ignore
+        # Enable mathjax only if equations exists
         options = {'async': 'async'}
         if app.config.mathjax_options:
             options.update(app.config.mathjax_options)
@@ -101,6 +104,6 @@ def setup(app):
     app.add_config_value('mathjax_inline', [r'\(', r'\)'], 'html')
     app.add_config_value('mathjax_display', [r'\[', r'\]'], 'html')
     app.add_config_value('mathjax_config', None, 'html')
-    app.connect('builder-inited', builder_inited)
+    app.connect('env-check-consistency', install_mathjax)
 
     return {'version': sphinx.__display_version__, 'parallel_read_safe': True}


Test patch:
diff --git a/tests/test_ext_math.py b/tests/test_ext_math.py
index 738ad8ce6..5ccaa6047 100644
--- a/tests/test_ext_math.py
+++ b/tests/test_ext_math.py
@@ -255,7 +255,7 @@ def test_math_compat(app, status, warning):
                      [nodes.math_block, "E = mc^2"]))
 
 
-@pytest.mark.sphinx('html', testroot='basic',
+@pytest.mark.sphinx('html', testroot='ext-math',
                     confoverrides={'extensions': ['sphinx.ext.mathjax'],
                                    'mathjax_config': {'extensions': ['tex2jax.js']}})
 def test_mathjax_config(app, status, warning):
@@ -265,3 +265,22 @@ def test_mathjax_config(app, status, warning):
     assert ('<script type="text/x-mathjax-config">'
             'MathJax.Hub.Config({"extensions": ["tex2jax.js"]})'
             '</script>' in content)
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+                    confoverrides={'extensions': ['sphinx.ext.mathjax']})
+def test_mathjax_is_not_installed_if_no_equations(app, status, warning):
+    app.builder.build_all()
+
+    content = (app.outdir / 'index.html').text()
+    assert 'MathJax.js' not in content
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+                    confoverrides={'extensions': ['sphinx.ext.jsmath'],
+                                   'jsmath_path': 'jsmath.js'})
+def test_jsmath_is_not_installed_if_no_equations(app, status, warning):
+    app.builder.build_all()
+
+    content = (app.outdir / 'index.html').text()
+    assert 'jsmath.js' not in content