Exemple #1
0
def setup(app: sphinx.application.Sphinx):
    """
    Sets up Sphinx extension.
    """
    app.connect("doctree-read", on_doctree_read)
    app.connect("builder-inited", on_builder_inited)
    app.add_css_file("uqbar.css")
Exemple #2
0
def _builder_inited(app: sphinx.application.Sphinx) -> None:
    if app.config.html_theme != "furo":
        return

    # Our `main.js` file needs to be loaded as soon as possible.
    app.add_js_file("scripts/main.js", priority=200)

    # 500 is the default priority for extensions, we want this after this.
    app.add_css_file("styles/furo-extensions.css", priority=600)

    builder = app.builder
    assert builder.dark_highlighter is None, "this shouldn't happen."

    # number_of_hours_spent_figuring_this_out = 7
    #
    # Hello human in the future! This next block of code needs a bit of a story, and
    # if you're going to touch it, remember to update the number above (or remove this
    # comment entirely).
    #
    # Hopefully, you know that Sphinx allows extensions and themes to add configuration
    # values via `app.add_config_value`. This usually lets users set those values from
    # `conf.py` while allowing the extension to read from it and utilise that information.
    # As any reasonable person who's written a Sphinx extension before, you would
    # expect the following to work:
    #
    #     dark_style = app.config.pygments_dark_style
    #
    # Turns out, no. How dare you expect things to just work!? That stuff just returns
    # the default value provided when calling `app.add_config_value`. Yes, even if you
    # set it in `conf.py`. Why? Good question. :)
    #
    # The logic in Sphinx literally looks it up in the same mapping as what was
    # manipulated by `add_config_value`, and there's no other spot where that value
    # gets manipulated. I spent a bunch of time debugging how that class works, and...
    # yea, I can't figure it out. There's multiple mappings floating around and bunch
    # of manipulation being done for all kinds of things.
    #
    # The only place on the config object where I was able to find the user-provided
    # value from `conf.py` is a private variable `self._raw_config`. Those values are
    # supposed to get added to self.__dict__[...], and generally be accessible through
    # the object's custom `__getattr__`.
    #
    # Anyway, after giving up on figuring out how to file a PR to fix this upstream, I
    # started looking for hacky ways to get this without reaching into private
    # variables. That quest led to a very simple conclusion: no, you can't do that.
    #
    # So, here we are: with the only option being to reach into the guts of the beast,
    # and pull out the specific thing that's needed. This is obviously fragile though,
    # so this is written with the assumption that any changes to Sphinx's config
    # object's internals would correspond to the originally expected behaviour working.
    # This is so that when any of Sphinx's internals change, this logic would basically
    # fall back to the original behaviour and also print a warning, so that hopefully
    # someone will report this. Maybe it'll all be fixed, and I can remove this whole
    # hack and this giant comment.
    #
    # But wait, this hack actually has another layer to it.
    #
    # This whole setup depends on an internal implementation detail in Sphinx -- that
    # it "adds" the `pygments_dark.css` file for inclusion in output, at a different
    # point than where it is generates the file. The dark syntax highlighting mechanism
    # of this theme depends on that fact -- we don't actually set `pygments_dark_style`
    # in our theme.conf file.
    #
    # Instead, we stick our filthy monkey hands into Sphinx's builder, to patch the
    # builder to generate the `pygments_dark.css` file as if this theme actually sets
    # `pygments_dark_style`. This results in Sphinx generating the file without
    # injecting a custom CSS file for it. Then, we include that stylesheet in our HTML
    # via a hand-crafted <link> tag. There's 2 benefits to this approach: (1) it works,
    # (2) we can, at some point in the future, pivot to a different strategy for
    # including the dark mode syntax highlighting styles.

    # HACK: begins here
    dark_style = None
    try:
        if (hasattr(app.config, "_raw_config")
                and isinstance(app.config._raw_config, dict)
                and "pygments_dark_style" in app.config._raw_config):
            dark_style = app.config._raw_config["pygments_dark_style"]
    except (AttributeError, KeyError) as e:
        logger.warn(
            ("Furo could not determine the value of `pygments_dark_style`. "
             "Falling back to using the value provided by Sphinx.\n"
             "Caused by %s"),
            e,
        )

    if dark_style is None:
        dark_style = app.config.pygments_dark_style

    builder.dark_highlighter = PygmentsBridge("html", dark_style)