Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardize logo image behavior between Sphinx and this theme #1132

Merged
merged 23 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
# a list of builtin themes.
#
html_theme = "pydata_sphinx_theme"
html_logo = "logo.svg"
html_logo = "_static/logo.svg"
12rambau marked this conversation as resolved.
Show resolved Hide resolved
html_favicon = "_static/logo.svg"
html_sourcelink_suffix = ""

Expand Down Expand Up @@ -124,7 +124,7 @@
],
"logo": {
"text": "PyData Theme",
"image_dark": "logo-dark.svg",
"image_dark": "_static/logo-dark.svg",
"alt_text": "PyData Theme",
},
"use_edit_page_button": True,
Expand Down
2 changes: 2 additions & 0 deletions docs/examples/pydata.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ file_format: mystnb
kernelspec:
name: python3
display_name: Python 3
mystnb:
execution_mode: cache
12rambau marked this conversation as resolved.
Show resolved Hide resolved
---

% To test this file with nbsphinx we need to convert to ipynb. To do this:
Expand Down
21 changes: 12 additions & 9 deletions docs/user_guide/branding.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ This can be replaced by a logo image, and optionally a custom ``html_title`` as
Single logo for light and dark mode
-----------------------------------

To use a local image file, put an image in a folder that is in `html_static_path`, and use the following configuration:
To use a **local image file**, use ``html_logo`` as specified in the `Sphinx documentation <https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_logo>`__.
The file must be relative to ``conf.py``.
For example, if your documentation had a logo in ``_static/logo.png``:

.. code:: python

html_static_path = ["_static"]
html_logo = "logo.png"
html_logo = "_static/logo.png"

To use an external link to an image, make sure the ``html_logo`` begins with ``http``.
To use an **external link** to an image, make sure the ``html_logo`` begins with ``http``.
For example:

.. code:: python
Expand All @@ -31,21 +32,23 @@ Different logos for light and dark mode
You may specify a different version of your logo image for "light" and "dark" modes.
This is useful if your logo image is not adapted to a dark mode (light background, not enough contrast, etc...).

To do so, put the 2 image files in a folder that is in ``html_static_path`` and configure the relative path to each image with ``logo["image_light"]`` and ``logo["image_dark"]`` in ``html_theme_options``, like so:
To do so, use the ``logo["image_light"]`` and ``logo["image_dark"]`` options in ``html_theme_options``.
For each, provide a path relative to ``conf.py`` like so:

.. code-block:: python

html_static_path = ["_static"]
# Assuming your `conf.py` has a sibling folder called `_static` with these files
html_theme_options = {
"logo": {
"image_light": "logo-light.png",
"image_dark": "logo-dark.png",
"image_light": "_static/logo-light.png",
"image_dark": "_static/logo-dark.png",
}
}

.. note::

``image_light`` and ``image_dark`` will override the ``html_logo`` setting. If you only specify one of the light or dark variants, the un-specified variant will fall back to the value of ``html_logo``.
``image_light`` and ``image_dark`` will override the ``html_logo`` setting.
If you only specify one of the light or dark variants, the un-specified variant will fall back to the value of ``html_logo``.

Customize logo link
-------------------
Expand Down
24 changes: 18 additions & 6 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Automatically build our documentation or run tests.

Environments are re-used by default. Use the following-pattern to re-install them.
Environments are re-used by default.

nox -s docs -- -r
Re-install the environment from scratch:

nox -s docs -- -r
"""
import nox
from pathlib import Path
Expand Down Expand Up @@ -33,7 +35,7 @@ def _should_install(session):
return should_install


@nox.session
@nox.session(name="compile")
def compile(session):
"""Compile the theme's web assets with sphinx-theme-builder."""
if _should_install(session):
Expand All @@ -42,12 +44,12 @@ def compile(session):
session.run("stb", "compile")


@nox.session
@nox.session(name="docs")
def docs(session):
"""Build the documentation and place in docs/_build/html."""
if _should_install(session):
session.install("-e", ".[doc]")
session.run("sphinx-build", "-b=html", "docs/", "docs/_build/html")
session.run("sphinx-build", "-b=html", "docs/", "docs/_build/html", "-v")


@nox.session(name="docs-live")
Expand All @@ -61,9 +63,19 @@ def docs_live(session):

@nox.session(name="test")
def test(session):
"""Run the test suite. Use `-- -r` to re-build the environment."""
"""Run the test suite."""
if _should_install(session):
session.install("-e", ".[test]")
session.run("pytest", *session.posargs)


@nox.session(name="test-sphinx")
@nox.parametrize("sphinx", ["4", "5", "6"])
def test_sphinx(session, sphinx):
"""Run the test suite with a specific version of Sphinx."""
if _should_install(session):
session.install("-e", ".[test]")
session.install(f"sphinx=={sphinx}")
session.run("pytest", *session.posargs)


Expand Down
70 changes: 69 additions & 1 deletion src/pydata_sphinx_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
from bs4 import BeautifulSoup as bs
from docutils import nodes
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.environment.adapters.toctree import TocTree
from sphinx.addnodes import toctree as toctree_node
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util.nodes import NodeMatcher
from sphinx.errors import ExtensionError
from sphinx.util import logging
from sphinx.util import logging, isurl
from sphinx.util.fileutil import copy_asset_file
from pygments.formatters import HtmlFormatter
from pygments.styles import get_all_styles
import requests
Expand Down Expand Up @@ -1084,6 +1086,70 @@ def setup_translators(app):
app.set_translator(name, translator, override=True)


# ------------------------------------------------------------------------------
# customize events for logo management
# we use one event to copy over custom logo images to _static
# and another even to link them in the html context
# ------------------------------------------------------------------------------


def setup_logo_path(
app: Sphinx, pagename: str, templatename: str, context: dict, doctree: nodes.Node
) -> None:
"""Set up relative paths to logos in our HTML templates.

In Sphinx, the context["logo"] is a path to the `html_logo` image now in the output
`_static` folder.

If logo["image_light"] and logo["image_dark"] are given, we must modify them to
follow the same pattern. They have already been copied to the output folder
in the `update_config` event.
"""

# get information from the context "logo_url" for sphinx>=6, "logo" sphinx<6
pathto = context.get("pathto")
logo = context.get("logo_url") or context.get("logo")
theme_logo = context.get("theme_logo", {})

# Define the final path to logo images in the HTML context
theme_logo["image_relative"] = {}
for kind in ["light", "dark"]:
image_kind_logo = theme_logo.get(f"image_{kind}")

# If it's a URL the "relative" path is just the URL
# else we need to calculate the relative path to a local file
if image_kind_logo:
if not isurl(image_kind_logo):
image_kind_name = Path(image_kind_logo).name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too restrictive, in that it doesn't allow paths relative to static. Is stripping the name needed at all?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strip the name is essential if you use files from outside the doc folder (i.e. the root of the repo) which would have path like "../../../my_hidden_logo.png". This file needs to end up in the build _static directory eventually

image_kind_logo = pathto(f"_static/{image_kind_name}", resource=True)
theme_logo["image_relative"][kind] = image_kind_logo

# If there's no custom logo for this kind, just use `html_logo`
# If `logo` is also None, then do not add this key to context.
elif isinstance(logo, str) and len(logo) > 0:
theme_logo["image_relative"][kind] = logo

# Update our context logo variables with the new image paths
context["theme_logo"] = theme_logo


def copy_logo_images(app: Sphinx, exception=None) -> None:
"""
If logo image paths are given, copy them to the `_static` folder
Then we can link to them directly in an html_page_context event
"""
theme_options = app.config.html_theme_options
logo = theme_options.get("logo", {})
staticdir = Path(app.builder.outdir) / "_static"
for kind in ["light", "dark"]:
path_image = logo.get(f"image_{kind}")
if not path_image or isurl(path_image):
continue
if not (Path(app.srcdir) / path_image).exists():
logger.warning(f"Path to {kind} image logo does not exist: {path_image}")
Comment on lines +1148 to +1149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There probably is not a good way to check if the path exists in a theme-supplied static directory? If not, then maybe just skip this warning or demote to INFO or DEBUG?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should this not continue at this point?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There probably is not a good way to check if the path exists in a theme-supplied static directory? If not, then maybe just skip this warning or demote to INFO or DEBUG?

I'm -1 on changing the warning, as you mentioned people are listening to warnings in their CI/CD it would mean that nobody would realize that the logos are not displayed anymore until someone check visually the logs/website.

Also, should this not continue at this point?

yes we should

copy_asset_file(path_image, staticdir)


# -----------------------------------------------------------------------------


Expand All @@ -1101,7 +1167,9 @@ def setup(app):
app.connect("html-page-context", add_toctree_functions)
app.connect("html-page-context", prepare_html_config)
app.connect("html-page-context", update_and_remove_templates)
app.connect("html-page-context", setup_logo_path)
app.connect("build-finished", _overwrite_pygments_css)
app.connect("build-finished", copy_logo_images)

# Include component templates
app.config.templates_path.append(str(theme_path / "components"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
{# Logo link generation -#}
{% if not theme_logo.get("link") %}
{% set href = pathto(root_doc) %}
{% elif hasdoc(theme_logo.get("link")) %}
{% set href = pathto(theme_logo.get("link")) %} {# internal page #}
{% else %}
{% set href = theme_logo.get("link") %} {# external url #}
{% endif %}

{#- Logo HTML and image #}
<a class="navbar-brand logo" href="{{ href }}">
{# get all the brand information from html_theme_option #}
{% set is_logo = logo_url or theme_logo.get("image_light") or theme_logo.get("image_dark") %}
{% set image_light = theme_logo.get("image_light") or logo_url %}
{% set image_dark = theme_logo.get("image_dark") or logo_url %}
{% set image_light = image_light if image_light.startswith("http") else pathto('_static/' + image_light, 1) %}
{% set image_dark = image_dark if image_dark.startswith("http") else pathto('_static/' + image_dark, 1) %}
{% set is_logo = "light" in theme_logo["image_relative"] %}
{% set alt = theme_logo.get("alt_text", "Logo image") %}
{% if is_logo %}
<img src="{{ image_light }}"
class="logo__image only-light"
alt="{{ alt }}"/>
<img src="{{ image_dark }}" class="logo__image only-dark" alt="{{ alt }}"/>
<img src="{{ theme_logo['image_relative']['light'] }}" class="logo__image only-light" alt="{{ alt }}"/>
<img src="{{ theme_logo['image_relative']['dark'] }}" class="logo__image only-dark" alt="{{ alt }}"/>
{% endif %}
{% if not is_logo or theme_logo.get("text") %}
<p class="title logo__title">{{ theme_logo.get("text") or docstitle }}</p>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/sites/base/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# ones.
extensions = []
html_theme = "pydata_sphinx_theme"
html_logo = "emptylogo.png"
html_logo = "_static/emptylogo.png"
html_copy_source = True
html_sourcelink_suffix = ""

Expand Down
19 changes: 18 additions & 1 deletion tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def test_logo_two_images(sphinx_build_factory):
"html_theme_options": {
"logo": {
"text": "Foo Title",
"image_dark": "emptydarklogo.png",
"image_dark": "_static/emptydarklogo.png",
}
},
}
Expand All @@ -201,6 +201,23 @@ def test_logo_two_images(sphinx_build_factory):
assert "Foo Title" in index_str


def test_logo_missing_image(sphinx_build_factory):
"""Test that a missing image will raise a warning."""
# Test with a specified title and a dark logo
confoverrides = {
"html_theme_options": {
"logo": {
# The logo is actually in _static
"image_dark": "emptydarklogo.png",
}
},
}
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build(
no_warning=False
)
assert "image logo does not exist" in escape_ansi(sphinx_build.warnings).strip()


def test_logo_external_link(sphinx_build_factory):
"""Test that the logo link is correct for external URLs."""
# Test with a specified external logo link
Expand Down