Sphinx: Include Mathjax.js on per-page basis

Created on 1 Apr 2019  路  12Comments  路  Source: sphinx-doc/sphinx

Is your feature request related to a problem? Please describe.

Currently in a documentation project we have many pages, of which only a very small subset needs mathjax. However, enabling the sphinx.ext.mathjax extension causes mathjax.js to be included in every page. This is a bit unfortunate since mathjax is huge.

Describe the solution you'd like

Mathjax.js is included in only the pages which need it, and not every page.

Describe alternatives you've considered

Current system works, but makes all pages that don't need mathjax bigger and slower than they need to.

Additional context

I mentioned this as a comment in #5497 , where it was suggested that I open a new issue.

enhancement extensions

Most helpful comment

  • If the function returns not None then it does nothing.

  • If the function returns None, then it removes the JS/CSS file from the page context

I'd use boolean values instead, but otherwise I like it.

If we're willing to be a bit... uhm... YOLO about this, we could even have the condition argument be added as a kw-only argument to add_js_file. I don't imagine anyone is setting that attribute on a JS file.

All 12 comments

https://github.com/executablebooks/sphinx-panels/issues/28#issuecomment-683748518

An idea for a Sphinx plugin that does what's been requested here.

Thinking about possible plugin implementations, would hooking into html-page-context be a good approach here? This is called on a per-page basis, and has the advantage that the content isn't mutated afterwards.

In order to not change the order of libraries and stylesheets included, I think conditionally removing js would work better, and the plugin configuration could specify a doctree node selector that is required to keep a js/css file.

Yup -- that's exactly what I had in mind -- filtering the JS files "out" based on some conditional for them. Something like "only remove if there's no .math elements".

The exact design is 100% in the air though -- I imagine callable functions maybe nice in the spirit of extensibility but that's complexity and so on. :)

Thinking about sphinx design, I'd say using any condition accepted by doctree.traverse should do. That would allow both the simple option of specifying node types, and arbitrary more complex ones.

I think the pattern you suggested to remove files would work. I just tried adding an html-page-context listener to conditionally remove another JS library (thebelab) when a page didn't have the node that would trigger it, and it seemed to work nicely 馃憤

Here's the code I used in case it's helpful:

def remove_thebe_if_not_needed(app, pagename, templatename, context, doctree):
    if not doctree:
        return

    if not doctree.traverse(ThebeButtonNode):
        # Remove thebe JS files
        new_script_files = []
        for ii in context["script_files"]:
            if ii not in ["_static/sphinx-thebe.js", "https://unpkg.com/thebelab@latest/lib/index.js"]:
                new_script_files.append(ii)
        context["script_files"] = new_script_files

        # Remove thebe CSS files
        new_css_files = []
        for ii in context["css_files"]:
            if ii not in ['_static/thebelab.css', '_static/sphinx-thebe.css']:
                new_css_files.append(ii)
        context["css_files"] = new_css_files

Sweet! Now the real question is if it's even worth an extension of its own鈥攖he amount of code is minimal.

It would potentially make sense to make it a part of sphinx itself by allowing js and css files also specify a doctree selector for themselves to be included.

Not sure what would be a good interface. The obvious option is to allow

html_js_files = [
    'js/custom.js',  # included unconditionally
    ('js/large_optional_library.js', library_selector),  # only if selector isn't empty
]

This is backwards compatible, but ugly.

hmmmm, I will think about it a bit more...another option is something like:

  • an extension could provide its own function to add files like add_conditional_js_file or something
  • that would have all the same arguments as add_js_file except for another one called condition which would be a callable that takes all of the arguments of html-page-context
  • The extension adds an event callback for html-page-context. In that callback, the user-provided callable is called.
  • If the function returns not None then it does nothing.
  • If the function returns None, then it removes the JS/CSS file from the page context

Then extension authors could do whatever they want with callable. E.g. some might want the JS loaded only on pages that meet a user-provided variable like load_js_on_pages = ["index", "config"]. Others might want to parse the doctree. Others might want to check for a context variable like load_library=True.

  • If the function returns not None then it does nothing.

  • If the function returns None, then it removes the JS/CSS file from the page context

I'd use boolean values instead, but otherwise I like it.

If we're willing to be a bit... uhm... YOLO about this, we could even have the condition argument be added as a kw-only argument to add_js_file. I don't imagine anyone is setting that attribute on a JS file.

Does add_js_file cover the user-provided html_js_files?

Also: should the condition take all arguments passed to html-page-context or just a doctree traverse selector (that one is also callable and may do arbitrary stuff on the doctree).

What if we substituted

app.connect('env-updated', install_mathjax)

for

app.connect('html-page-context', install_mathjax)

That would provide the page name, and then the check could dive into whether that page has math tags?

I am prototyping this over here, if any others have thoughts or contributions: https://github.com/executablebooks/sphinx-conditional-asset/blob/main/sphinx_conditional_asset/__init__.py#L11

My plan is to release it as an extension once we have a pattern that makes sense, and then if Sphinx core thinks it'd be good to have, upstream it.

Was this page helpful?
0 / 5 - 0 ratings