diff --git a/docs/_data/config-header.yml b/docs/_data/config-header.yml new file mode 100644 index 00000000..da60fbd5 --- /dev/null +++ b/docs/_data/config-header.yml @@ -0,0 +1,48 @@ +brand: + type: button + image: https://executablebooks.org/en/latest/_static/logo.svg + url: https://sphinx-book-theme.readthedocs.io + # For testing + # content: Test site name +start: + - type: dropdown + content: Projects + items: + - content: Jupyter Book + url: https://jupyterbook.org + - content: MyST Markdown + url: https://myst-parser.readthedocs.io + - content: MyST-NB + url: https://myst-nb.readthedocs.io + + - type: dropdown + content: Community + items: + - content: Community guide + url: https://executablebooks.org + - content: Forum + url: https://github.com/orgs/executablebooks/discussions + - content: Feature Voting + url: https://executablebooks.org/en/latest/feature-vote.html + + - type: button + content: Book gallery + url: http://gallery.jupyterbook.org/ + + +end: + - type: group + items: + - type: button + icon: fab fa-twitter-square + title: Twitter + url: https://twitter.com/executablebooks + - type: button + icon: fab fa-github-square + title: GitHub + url: https://github.com/executablebooks + - type: button + icon: fas fa-comments + outline: true + content: Discussion + url: https://github.com/orgs/executablebooks/discussions diff --git a/docs/_static/custom.css b/docs/_static/custom.css index f0483400..ac67b7b1 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,3 +1,6 @@ :root { - --sd-color-primary: #f37726; + /* WONT WORK UNTIL THE NEXT PYDATA THEME IS RELEASED + --sbt-color-primary: #f37726; + --sd-color-primary: var(--sbt-color-primary); + */ } diff --git a/docs/_templates/sections/header.html b/docs/_templates/sections/header.html deleted file mode 100644 index 6630c176..00000000 --- a/docs/_templates/sections/header.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/docs/conf.py b/docs/conf.py index b9fd713f..dcf1a66f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,6 +2,7 @@ import os from pathlib import Path from urllib import request +from yaml import safe_load project = "Sphinx Book Theme" copyright = "2020" @@ -98,23 +99,40 @@ html_static_path = ["_static"] html_css_files = ["custom.css"] jupyter_execute_notebooks = "cache" -thebe_config = { - "repository_url": "https://github.com/binder-examples/jupyter-stacks-datascience", - "repository_branch": "master", -} + +header_config = safe_load(Path("./_data/config-header.yml").read_text()) html_theme_options = { "path_to_docs": "docs", "repository_url": "https://github.com/executablebooks/sphinx-book-theme", + "repository_branch": "master", # "repository_branch": "gh-pages", # For testing - "launch_buttons": { - "binderhub_url": "https://mybinder.org", - "colab_url": "https://colab.research.google.com/", - "deepnote_url": "https://deepnote.com/", - "notebook_interface": "jupyterlab", - "thebe": True, - # "jupyterhub_url": "https://datahub.berkeley.edu", # For testing - }, + "launch_buttons": [ + { + "type": "binderhub", + "hub_url": "https://mybinder.org", + "notebook_interface": "jupyterlab", + "content": "Launch in Lab", + }, + { + "type": "binderhub", + "hub_url": "https://mybinder.org", + "notebook_interface": "classic", + "content": "Launch in Notebook", + }, + { + "type": "jupyterhub", + "hub_url": "https://datahub.berkeley.edu", + }, # For testing + {"type": "colab"}, + {"type": "deepnote"}, + { + "type": "thebe", + "hub_url": "https://mybinder.org", + "repository_url": "https://github.com/binder-examples/jupyter-stacks-datascience", # noqa + "repository_branch": "master", + }, + ], "use_edit_page_button": True, "use_issues_button": True, "use_repository_button": True, @@ -126,6 +144,7 @@ "⚠️The latest release refactored our HTML, " "so double-check your custom CSS rules!⚠️" ), + "header": header_config, # For testing # "use_fullscreen_button": False, # "home_page_in_toc": True, diff --git a/docs/customize/header.md b/docs/customize/header.md index 0dc58c2b..7dde68a7 100644 --- a/docs/customize/header.md +++ b/docs/customize/header.md @@ -1,7 +1,346 @@ # Header content -By default, this theme does not contain any header content, it only has a sidebar and a main content window. -However, you can define your own HTML in a header that will be inserted **above everything else**. +A header extends the interface of your documentation to provide high-level information and links for readers at the top of the page. +They are often used to provide organization-wide branding, cross-links between documentation, and links to social media and external websites. + +Your header will be displayed above the sidebar and article content, but will disappear as readers scroll down. +On mobile displays, the header will be collapsed with a button to expand vertically. + +## Enable and configure the header + +Enable headers in your documentation by providing **a header configuration** in `conf.py`: + +```python +html_theme_options = { + "header": { } +} +``` + +For one example, see the header configuration of this documentation, in YAML format. + +````{admonition} YAML configuration for this theme's header +:class: dropdown +Below is YAML configuration for this theme's header. +It is read by `conf.py` and converted into a Python dictionary at build time. + +```{literalinclude} ../_data/config-header.yml +:language: yaml +``` +```` + +See the rest of these sections for how to add various elements to your header. + +## Header sections + +There are three major sections that you can control with your header: + +- [`brand`](header:brand): A special section for displaying a logo or site brand. +- [`start`](header:start): Left-aligned header components. +- [`end`](header:end): Right-aligned header components. + +Each section has its own configuration, which is specified via keys in the `header` configuration, like so: + +```python +html_theme_options = { + "header": { + "brand": { }, + "start": [ ], + "end": [ ], + } +} +``` + +Where the values of `"start":` and `"end":` are both [lists of component configuration items](header:components). + +(header:brand)= +### `brand` section + +The "brand" section is another place to put your title / logo if you don't want it to be in the primary sidebar (or, a place to put a higher-level logo like an organization-wide logo). +It is centered above your sidebar, and displayed to the left on mobile. + +#### Add an image logo + +To add a logo image to the brand section, see this sample configuration: + +```python +html_theme_options = { + "header": { + "brand": { + # Specifies that the brand area will use an image logo + "type": "link", + # Source of the image to be used + "image": "https://executablebooks.org/en/latest/_static/logo.svg", + # Link for the image + "url": "https://sphinx-book-theme.readthedocs.io", + }, + } +} +``` + +#### Add brand text + +To add text instead of an image, use the following configuration. +You can put arbitrary HTML in the `content` configuration: + +```python +html_theme_options = { + "header": { + "brand": { + # Specifies that we will use text instead of an image logo + "type": "text", + # Text that will be displayed + "content": "My documentation!", + # Link for the image + "url": "https://sphinx-book-theme.readthedocs.io", + }, + } +} +``` + +(header:start)= +### `start` section + +Your header's start section will be left-aligned with your article content (when the sidebar is present). +On mobile devices, it will be hidden under a collapsible button. + +To add components to your header's start section, use the following configuration: + +```python +html_theme_options = { + "header": { + "start": [ ] + } +} +``` + +(header:end)= +### `end` section + + +Your header's end section will be right-aligned with the page. +On mobile devices, it will be hidden under a collapsible button. + +To add components to your header's end section, use the following configuration: + +```python +html_theme_options = { + "header": { + "end": [ ] + } +} +``` + +(header:components)= +## Components + +Components are small UI elements that can be added to your header's sections. + +Add components to the two major sections of your header ([`start`](header:start) and [`end`](header:end)) by providing **lists of component configuration**. +Each component configuration takes a **`type:`** key to specify what type of component it is, as well as a collection of **`key:val`** parameters that modify the component's behavior. + +For example, the following configuration adds two link components to the "start" section of a header, in `conf.py`: + +```python +html_theme_options = { + "header": { + "start": [ + { + "type": "text", + "url": "https://executablebooks.org", + "content": "Executable Books" + }, + { + "type": "text", + "url": "https://jupyterbook.org", + "content": "Jupyter Book" + }, + ] + } +} +``` + +The rest of these sections describe the components you can use: + +### Buttons + +Buttons are flexible UI components that trigger various actions when clicked. +They can be links, or they can trigger arbitrary JavaScript that you provide. +Use them as call-to-action items, or as a way to trigger events on your page if you are using custom JavaScript. + +Clicking a button can trigger one of two actions: + +- **Link to an internal or external page** +- **Trigger a JavaScript function** + +Buttons have three visual sections that you may control, that roughly follow this structure: + +```html + +``` + +- **`icon`**: A small, square icon. May be configured with **FontAwesome** icons or a path to an image (local or remove). +- **`image`**: A larger image that may be rectangular. May be configured with **FontAwesome** icons or a path to an image (local or remove). +- **`content`**: Arbitrary text (or extra HTML) to put inside the button. + +To add buttons components to header sections, use the following component configuration: + +```python + +# Provided as a list item to `start:` or `end:` +{ + # Specifies the `button` component + "type": "button", + + ## Controls button behavior ## + # If provided, the button will be a link to another page. + "url": "https://google.com", + # If provided, clicking the button will trigger this JavaScript. + "onclick": "SomeFunction()", + + ## Controls button style ## + # An icon that will be displayed before the text + # Can be a set of fontawesome classes, or a path to an image + "icon": "fas fa-bars", + # An image path that will be displayed before the text + "image": "https://executablebooks.org/en/latest/_static/logo.svg", + # The text that will be displayed inside the button. + "content": "My button's text", + # An optional list of classes to add + "classes": ["one", "two"] +} +``` + +Note tha **url** and **onclick** cannot both be provided in the same button's configuration. + +#### Button icons + +There are two kinds of icons you can control with the `icon` parameter: + +- **FontAwesome icons**: FontAwesome icon classes like `fas fa-arrow-right`. See [the FontAwesome documentation](https://fontawesome.com/icons) for a list of classes and icons. For example: + + ```python + # Provided as a list item to `start:` or `end:` + { + # Specifies the `button` component + "type": "button", + "icon": "fab fa-github" + "url": "https://github.com" + } + ``` +- **A path to an image**: Any local image you include with your documentation. + + For example, with a remote image: + + ```python + # Provided as a list item to `start:` or `end:` + { + # Specifies the `button` component + "type": "button", + "image": "https://executablebooks.org/en/latest/_static/logo.svg" + "url": "https://executablebooks.org" + } + ``` + + With a local image: + + ```python + # Provided as a list item to `start:` or `end:` + { + # Specifies the `button` component + "type": "button", + "image": "./_static/myimage.png" + "url": "https://executablebooks.org" + } + ``` + +Finally, if a button is configured **only with an icon**, it will have a special class `icon-button` added to it that will make it contract slightly in size and spacing. + +### Dropdown menus + +Dropdown menus provide a clickable button that will display a list of links. +It is a useful way to provide more links in your header without using too much horizontal space. + +To add dropdown components to header sections, use the following component configuration: + +```python +# Provided as a list item to `start:` or `end:` +{ + # Specifies a `dropdown` type + "type": "dropdown", + # Text to be displayed on the button + "content": "EBP Projects", + # A list of dropdown links. Each is a configuration for a button. + "items": [ + { + "url": "https://executablebooks.org", + "content": "Executable Books" + }, + { + "url": "https://jupyterbook.org", + "content": "Jupyter Book" + }, + ], + # An optional list of classes to add + "classes": ["one", "two"] +}, +``` + +### Groups + +Groups are a way of telling the theme that several UI components should be grouped together. +They will have a wrapping container, will have less spacing between them, and will be displayed _horizontally_ on narrow screens. + +For example, to group several icon buttons together use a configuration like so: + +```python +# Provided as a list item to `start:` or `end:` +{ + # Specifies a `group` type + "type": "group", + # A list of group items. Each is a configuration for a button. + "items": [ + { + "icon": "fab fa-github", + "url": "https://github.com + }, + { + "icon": "fab fa-twitter", + "url": "https://twitter.com + }, + { + "icon": "fab fa-discourse", + "url": "https://discourse.com + }, + ], + # An optional list of classes to add + "classes": ["one", "two"] +}, +``` + +### HTML Snippets + +You may provide custom HTML snippets that are inserted into the header as-is. +These are useful to define your own components or styling. + +To add raw HTML components to header sections, use the following component configuration: + +```python +# Provided as a list item to `start:` or `end:` +{ + # Specifies a `html` component + "type": "html", + # The HTML to be inserted + "html": "My custom span", +}, +``` + +## Over-write the header entirely + +Instead of using any of the above functionality, you can also provide your own raw HTML for the header. +Use it if you need to have much more flexibility and control over the header. + To do so, you must define your own Sphinx Template in a specific location. The contents of this template will be inserted into the theme. Here is how to do this: @@ -23,8 +362,3 @@ Here is how to do this: $ echo "

Some text!

" > _templates/sections/header.html ``` - Build your documentation, and you should see content of this file show up above your site. - -## Style the header - -Note that the header has very little styling applied to it by default. -So you should [add custom styles to the theme](custom-css.md) in order to achieve the look and feel you want. diff --git a/docs/launch.md b/docs/launch.md index 476f8077..ca6ff301 100644 --- a/docs/launch.md +++ b/docs/launch.md @@ -1,19 +1,25 @@ (customize:launch)= # Launch buttons for interactivity -You can automatically add buttons that allow users to interact with your -book's content. This is either by directing them to a BinderHub or JupyterHub -that runs in the cloud, or by making your page interactive using Thebe. +Launch buttons are a way to connect pages have computational content with environments that let readers execute and edit code interactively. +These use a variety of services that connect the user with a live kernel. -To use either Binder or JupyterHub links, you'll first need to configure your -documentation's repository url: +## Common configuration + +There is some configuration that will be applied to almost all launch buttons. +To **over-ride these global values for each button**, you can manually specify different values within each launch button's configuration. + +Here are the global configuration variables: ```python html_theme_options = { ... + # The location of your documentation relative to the repository root + "path_to_docs": "docs/", + # The URL where your documentation's content exists "repository_url": "https://github.com/{your-docs-url}", + # The branch where your documentation's content exists "repository_branch": "{your-branch}", - "path_to_docs": "{path-relative-to-site-root}, ... } ``` @@ -26,110 +32,102 @@ folder as your content, then Binder/JupyterHub links will point to the ipynb file instead of the text file. ``` -## Binder / BinderHub - -To add Binder links your page, add the following configuration: - -```python -html_theme_options = { - ... - "launch_buttons": { - "binderhub_url": "https://{your-binderhub-url}" - }, - ... -} -``` +## Launch button configuration structure -## JupyterHub +Add launch buttons added by providing a **list of launch button configuration dictionaries** to the `html_theme_options.launch_buttons` configuration. -To add JupyterHub links to your page, add the following configuration: +For example, the following configuration specifies two different kinds of JupyterHub buttons: ```python html_theme_options = { - ... - "launch_buttons": { - "jupyterhub_url": "https://{your-binderhub-url}" - }, - ... + "launch_buttons": [ + { + "type": "jupyterhubhub", + "hub_url": "https://myjupyterhub.org", + }, + { + "type": "jupyterhub", + "hub_url": "https://myotherjupyterhub.org",, + }, + ] } ``` -## Google Colab +There is some configuration that is specific to a given launch button, described below. +The remaining sections of this page describe how to add various launch buttons. -To add Google Colab links to your page, add the following configuration: +## Binder / BinderHub + +To add Binder links your page, add the following configuration: ```python html_theme_options = { - ... - "launch_buttons": { - "colab_url": "https://{your-colab-url}" - }, - ... + "launch_buttons": [ + ..., + { + # Specifies a binderhub launch button + "type": "binderhub", + # The URL of the binderhub where a session is launched + "hub_url": "https://{your-binderhub-url}", + # The notebook interface used when somebody clicks on a launch button. + # Must be one of `jupyterlab` or `classic` + "notebook_interface": "jupyterlab", + }, + ... + ] } ``` -## Deepnote -To add [Deepnote](https://deepnote.com) links to your page, add the following configuration: +## JupyterHub + +To add JupyterHub links to your page, add the following configuration: ```python html_theme_options = { - ... - "launch_buttons": { - "deepnote_url": "https://deepnote.com" - }, - ... + "launch_buttons": [ + ..., + { + # Specifies a jupyterhub launch button + "type": "jupyterhub", + # The URL of the hub where a session is launched + "hub_url": "https://{your-jupyterhub-url}", + # The notebook interface used when somebody clicks on a launch button. + # Must be one of `jupyterlab` or `classic` + "notebook_interface": "jupyterlab", + }, + ... + ] } ``` -```{warning} -This will create a new Deepnote project every time you click the launch button. -``` - -## Live code cells with Thebe +## Interactive code cells with Thebe [Thebe](http://thebe.readthedocs.org/) converts your static code blocks into -*interactive* code blocks powered by a Jupyter kernel. It does this by asking for a BinderHub kernel -*under the hood* and converts all of your -code cells into *interactive* code cells. This allows users to run the code on -your page without leaving the page. +*interactive* code blocks powered by a Jupyter kernel. +It does this by asking for a BinderHub kernel *under the hood* and converts all of your code cells into *interactive* code cells. +This allows users to run the code on your page without leaving the page. -You can use the Sphinx extension -[`sphinx-thebe`](https://sphinx-thebe.readthedocs.io/en/latest/) to add -live code functionality to your documentation. You can install `sphinx-thebe` from `pip`, -then activate it by putting it in your `conf.py` extensions list: +To use interactive code cells, first ensure that [`sphinx-thebe`](https://sphinx-thebe.readthedocs.io/en/latest/) is installed: -```python -extensions = [ - ... - "sphinx_thebe" - ... -] +```console +$ pip install sphinx-thebe ``` -If you'd like to activate UI elements for `sphinx-thebe` in the `sphinx-book-theme`, -add the following theme configuration: +To add a {guilabel}`Interactive Code` button to your launch buttons, use the following configuration: ```python html_theme_options = { - ... - "launch_buttons": { - "thebe": True, - }, - ... -} -``` - -This will add a custom launch button and some UI elements will be added for Thebe. - -If you also specify a `repository_url` with your theme configuration, `sphinx-thebe` -will use this repository for its environment: - -```python -html_theme_options = { - ... - "repository_url": "https://github.com/{your-docs-url}", - ... + "launch_buttons": [ + ..., + { + # Specifies a jupyterhub launch button + "type": "thebe", + # The URL of the binderhub that will serve your kernel + "hub_url": "https://{your-jupyterhub-url}", + }, + ... + ] } ``` @@ -140,29 +138,37 @@ configuration. See the [`sphinx-thebe`](https://sphinx-thebe.readthedocs.io/en/l documentation for what you can configure. ``` -## Configure a relative path to your source file -To configure a relative path to your documentation, add the following configuration: +## Google Colab + +To add Google Colab links to your page, add the following configuration: ```python html_theme_options = { - ... - "path_to_docs" = "{path-relative-to-repo-root}" - ... + "launch_buttons": [ + ..., + { + "type": "colab", + }, + ... + ] } ``` -## Control the user interface that is opened +## Deepnote -You can control the interface that is opened when somebody clicks on a launch button. -To do so, add the following configuration: +To add [Deepnote](https://deepnote.com) links to your page, add the following configuration: ```python html_theme_options = { ... "launch_buttons": { - "notebook_interface": "jupyterlab", + "type": "deepnote", }, ... } ``` + +```{warning} +This will create a new Deepnote project every time you click the launch button. +``` diff --git a/src/sphinx_book_theme/__init__.py b/src/sphinx_book_theme/__init__.py index 4b60d0a2..1e15ed6f 100644 --- a/src/sphinx_book_theme/__init__.py +++ b/src/sphinx_book_theme/__init__.py @@ -10,10 +10,11 @@ from sphinx.locale import get_translation from sphinx.util import logging -from .nodes import SideNoteNode +from .margin.nodes import SideNoteNode from .header_buttons import prep_header_buttons, add_header_buttons -from .header_buttons.launch import add_launch_buttons -from ._transforms import HandleFootnoteTransform +from .header_buttons._launch import add_launch_buttons, update_launch_button_config +from .margin._transforms import HandleFootnoteTransform +from .header_buttons._components import COMPONENT_FUNCS __version__ = "0.3.2" """sphinx-book-theme version""" @@ -62,6 +63,25 @@ def add_metadata_to_page(app, pagename, templatename, context, doctree): context.get("theme_search_bar_text", "Search the docs ...") ) + # Define the function render the above + def render_component(component): + component_copy = component.copy() + + # We use `type` to denote different kinds of components + kind = component_copy.pop("type") + if kind not in COMPONENT_FUNCS: + SPHINX_LOGGER.warn(f"Unknown component type: {kind}") + return + try: + output = COMPONENT_FUNCS[kind](app, context, **component_copy) + except Exception as exc: + msg = f"Component render failure for:\n{component}\n\n" + SPHINX_LOGGER.warn(msg) + raise exc + return output + + context["theme_render_component"] = render_component + @lru_cache(maxsize=None) def _gen_hash(path: str) -> str: @@ -111,40 +131,6 @@ def hash_html_assets(app, pagename, templatename, context, doctree): hash_assets_for_files(assets, get_html_theme_path() / "static", context) -def update_thebe_config(app): - """Update thebe configuration with SBT-specific values""" - theme_options = app.env.config.html_theme_options - if theme_options.get("launch_buttons", {}).get("thebe") is True: - if not hasattr(app.env.config, "thebe_config"): - SPHINX_LOGGER.warning( - ( - "Thebe is activated but not added to extensions list. " - "Add `sphinx_thebe` to your site's extensions list." - ) - ) - return - # Will be empty if it doesn't exist - thebe_config = app.env.config.thebe_config - else: - return - - if not theme_options.get("launch_buttons", {}).get("thebe"): - return - - # Update the repository branch and URL - # Assume that if there's already a thebe_config, then we don't want to over-ride - if "repository_url" not in thebe_config: - thebe_config["repository_url"] = theme_options.get("repository_url") - if "repository_branch" not in thebe_config: - branch = theme_options.get("repository_branch") - if not branch: - # Explicitly check in case branch is "" - branch = "master" - thebe_config["repository_branch"] = branch - - app.env.config.thebe_config = thebe_config - - class Margin(Sidebar): """Goes in the margin to the right of the page.""" @@ -175,7 +161,7 @@ def setup(app: Sphinx): app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir) # Events - app.connect("builder-inited", update_thebe_config) + app.connect("builder-inited", update_launch_button_config) app.connect("html-page-context", add_metadata_to_page) app.connect("html-page-context", hash_html_assets) diff --git a/src/sphinx_book_theme/assets/scripts/index.js b/src/sphinx_book_theme/assets/scripts/index.js index a5b2a594..794cb936 100644 --- a/src/sphinx_book_theme/assets/scripts/index.js +++ b/src/sphinx_book_theme/assets/scripts/index.js @@ -56,7 +56,7 @@ var toggleFullScreen = () => { * the screen. */ var scrollToActive = () => { - var navbar = document.getElementById("site-navigation"); + var navbar = document.querySelector("#site-navigation .bd-sidebar__content"); var active_pages = navbar.querySelectorAll(".active"); var active_page = active_pages[active_pages.length - 1]; // Only scroll the navbar if the active link is lower than 50% of the page @@ -181,7 +181,7 @@ var initTooltips = () => { $(document).ready(function () { $('[data-toggle="tooltip"]').tooltip({ trigger: "hover", - delay: { show: 500, hide: 100 }, + delay: { show: 750, hide: 100 }, }); }); }; diff --git a/src/sphinx_book_theme/assets/styles/abstracts/_variables.scss b/src/sphinx_book_theme/assets/styles/abstracts/_variables.scss index bb2bbbfe..5e61c927 100644 --- a/src/sphinx_book_theme/assets/styles/abstracts/_variables.scss +++ b/src/sphinx_book_theme/assets/styles/abstracts/_variables.scss @@ -20,8 +20,9 @@ $zindex-offcanvas: 1100; // We increase this to be over the tooltips // Spacing $header-article-height: 3em; -$leftbar-width-mobile: 75%; -$leftbar-width-wide: 275px; +$sidebar-primary-width-mobile: 75%; +$sidebar-primary-width-wide: 275px; +$sidebar-primary-padding: 1rem 1rem 0 1.5rem; $toc-width-mobile: 75%; // Main content, to leave room for the margin $content-max-width: 70%; @@ -64,7 +65,14 @@ $border-thin: 1px solid rgba(0, 0, 0, 0.1); // Variables for this theme --sbt-sidebar-font-size: var(--sbt-font-size-small-1); - --sbt-header-article-font-size: var(--sbt-font-size-small-1); --sbt-prevnext-font-size: var(--sbt-font-size-small-1); --sbt-footer-font-size: var(--sbt-font-size-small-1); + --sbt-header-font-size: var(--sbt-font-size-regular); + --sbt-font-size-menu-mobile: 1.1rem; + + // Header variables + --theme-header-article-font-size: var(--sbt-font-size-small-1); + --theme-header-button-color: #5a5a5a; + --theme-header-button-color-hover: black; + --theme-header-background-color: white; } diff --git a/src/sphinx_book_theme/assets/styles/base/_base.scss b/src/sphinx_book_theme/assets/styles/base/_base.scss index 75653d23..3d6a0166 100644 --- a/src/sphinx_book_theme/assets/styles/base/_base.scss +++ b/src/sphinx_book_theme/assets/styles/base/_base.scss @@ -3,11 +3,8 @@ *********************************************/ // For the helper pixel that we can watch to decide whether we've scrolled .sbt-scroll-pixel-helper { - position: absolute; width: 0px; height: 0px; - top: 0; - left: 0; } // Hide an element without display: none but so that it takes no space diff --git a/src/sphinx_book_theme/assets/styles/components/_buttons.scss b/src/sphinx_book_theme/assets/styles/components/_buttons.scss index fba7a86d..8c66e692 100644 --- a/src/sphinx_book_theme/assets/styles/components/_buttons.scss +++ b/src/sphinx_book_theme/assets/styles/components/_buttons.scss @@ -1,119 +1,125 @@ /********************************************* * Buttons, mostly for the headers * *********************************************/ + /** - * Basic button style + * In-page table of contents */ -.headerbtn { - display: flex; - align-items: center; - justify-content: center; - - background-color: white; - color: $non-content-grey; - cursor: pointer; - border: none; - padding: 0.1rem 0.5rem; // Horizontal padding since labels have none - margin: 0 0.1rem; - - span { - display: flex; - align-items: center; - } - - // Icons and image icons - img, - i { - margin: auto; - width: 1em; - text-align: center; - font-size: 1.5em; // Slightly larger for icons +#header__page-toc-button { + // Hide the button on wide screens since we display the TOC. + display: block; + @media (min-width: $breakpoint-md) { + display: none; } } /** - * Dropdown groups of buttons + * Header sections */ -.menu-dropdown__trigger:hover + .menu-dropdown__content, -.menu-dropdown__content:hover { - visibility: visible; - opacity: 1; + +// Reset default button styling on Safari because it has opinionated / clashing design +[type="button"] { + -webkit-appearance: none; } -.menu-dropdown__content { - // Hide by default, we'll show on hover - position: absolute; - visibility: hidden; - opacity: 0; - transform: translateX(-75%); - transition: opacity 0.2s ease-out; - - // Spacing and position - width: 10rem; - border-radius: $box-border-radius; - box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25); - padding: 0.5em; - - // Style - background-color: white; - - .headerbtn { - justify-content: left; - padding: 0.1rem 0rem; +div.dropdown { + // Control the dropdown menu display + + // First, overwrite the Bootstrap default so we can make nicer animations + // These make it "displayed" but hidden + .dropdown-menu { + display: block; + visibility: hidden; + opacity: 0; + transition: visibility 50ms ease-out, opacity 150ms ease-out 50ms; } - ul { - list-style: none; - padding-left: 0; - margin-bottom: 0; + // On hover, we display the contents via visibility and opacity + &:hover div.dropdown-menu, + div.dropdown-menu:hover { + visibility: visible; + opacity: 1; + // Remove the delay when hovering, which makes it appear instantly, but delay in disappear + transition-delay: 0ms; } - span { + // Other styling on the dropdown menu + .dropdown-menu { + // Copied from dropdown menu style above + border-radius: $box-border-radius; + box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25); + min-width: 13rem; display: flex; - - &.headerbtn__icon-container { - width: 2em; + flex-direction: column; + gap: 0.25rem; + + button { + display: flex; + align-items: center; + padding-left: 1rem; + width: 100%; + border-radius: 0; + + &:hover { + background-color: #eee; + } } - &.headerbtn__text-container { - flex-grow: 1; - margin-left: 0.5em; + a:hover { + text-decoration: none; } - } - i, - img { - font-size: 1.2em; // Slightly smaller icons in a dropdown - } -} + span.btn__icon-container { + display: flex; -// HACK: Need this to be extra-selective to over-ride some too-specific PST CSS -div.header-article-main { - .header-article__left, - .header-article__right { - a, - button, - label { - color: $non-content-grey; - - // Over-ride bootstrap defaults for clicking - &:hover, - &:focus { - color: black; - box-shadow: none; - text-decoration: none; + img, + i { + width: 1.2rem; // Slightly wider to make the font match the images + font-size: 1rem; } } } } -/** - * In-page table of contents +/* + * Button types classes */ -.headerbtn-page-toc { - // Hide the button on wide screens since we display the TOC. - display: block; - @media (min-width: $breakpoint-md) { - display: none; +// Basic button class +.icon-button { + padding: 0 0.2em; + margin-bottom: 0; + color: var(--theme-header-button-color); + + // Over-ride bootstrap defaults for clicking + &:hover, + &:focus { + color: var(--theme-header-button-color-hover); + box-shadow: none; + text-decoration: none; + } + + // Spacing for the icon if it's present with text + .btn__icon-container + .btn__content-container { + margin-left: 0.5rem; + } + + // Over-rides a pydata theme default to center it + .btn__content-container { + text-align: left; + } +} + +// If no content, make the icon a bit bigger +.icon-button-no-content { + font-size: 1.3rem; +} + +// Outline buttons need extra spacing +button.btn-outline { + outline: 1px solid $non-content-grey; + padding: 0.3rem 0.5rem; + + &:hover { + background-color: #ededed; } } diff --git a/src/sphinx_book_theme/assets/styles/index.scss b/src/sphinx_book_theme/assets/styles/index.scss index 8219bd1a..fd29cbf6 100644 --- a/src/sphinx_book_theme/assets/styles/index.scss +++ b/src/sphinx_book_theme/assets/styles/index.scss @@ -20,8 +20,9 @@ @import "sections/article"; @import "sections/footer-article"; @import "sections/footer-content"; +@import "sections/header"; +@import "sections/header-announcement"; @import "sections/header-article"; -@import "sections/headers"; @import "sections/sidebar-primary"; @import "sections/sidebar-secondary"; @import "sections/sidebars-toggle"; diff --git a/src/sphinx_book_theme/assets/styles/sections/_article.scss b/src/sphinx_book_theme/assets/styles/sections/_article.scss index b87e9c89..a7b5290c 100644 --- a/src/sphinx_book_theme/assets/styles/sections/_article.scss +++ b/src/sphinx_book_theme/assets/styles/sections/_article.scss @@ -12,6 +12,11 @@ body { padding-top: 1.5em; } +.content-container { + display: flex; + flex-direction: column; +} + // On wide screens, make space for the right margin @media (min-width: $breakpoint-md) { #main-content { diff --git a/src/sphinx_book_theme/assets/styles/sections/_footer-content.scss b/src/sphinx_book_theme/assets/styles/sections/_footer-content.scss index 6c147a44..97865889 100644 --- a/src/sphinx_book_theme/assets/styles/sections/_footer-content.scss +++ b/src/sphinx_book_theme/assets/styles/sections/_footer-content.scss @@ -1,6 +1,10 @@ /********************************************* * Footer - content * *********************************************/ +div.footer-content { + margin-top: auto; +} + footer { font-size: var(--sbt-font-size-small-1); } diff --git a/src/sphinx_book_theme/assets/styles/sections/_header-announcement.scss b/src/sphinx_book_theme/assets/styles/sections/_header-announcement.scss new file mode 100644 index 00000000..873fef86 --- /dev/null +++ b/src/sphinx_book_theme/assets/styles/sections/_header-announcement.scss @@ -0,0 +1,12 @@ +.announcement { + width: 100%; + text-align: center; + background-color: #616161; + color: white; + padding: 0.4em 12.5%; // Horizontal padding so the width is 75% + + @media (max-width: $breakpoint-md) { + // Announcements can take a bit more width on mobile + padding: 0.4em 2%; + } +} diff --git a/src/sphinx_book_theme/assets/styles/sections/_header-article.scss b/src/sphinx_book_theme/assets/styles/sections/_header-article.scss index 70cfc82f..62b815a1 100644 --- a/src/sphinx_book_theme/assets/styles/sections/_header-article.scss +++ b/src/sphinx_book_theme/assets/styles/sections/_header-article.scss @@ -4,9 +4,9 @@ *********************************************/ .header-article { height: $header-article-height; // Fix the height so the TOC doesn't grow it - background-color: white; + background-color: var(--theme-header-background-color); transition: left 0.2s; - font-size: var(--sbt-header-article-font-size); + font-size: var(--theme-header-article-font-size); @include header-height-mobile; @@ -26,6 +26,7 @@ .header-article__right { margin-left: auto; + gap: 0.5rem; } } } diff --git a/src/sphinx_book_theme/assets/styles/sections/_header.scss b/src/sphinx_book_theme/assets/styles/sections/_header.scss new file mode 100644 index 00000000..aabda23e --- /dev/null +++ b/src/sphinx_book_theme/assets/styles/sections/_header.scss @@ -0,0 +1,175 @@ +/** + * A few different header CSS rules + */ + +$header-default-height: 4rem; + +// Header container places the collapse button properly +.header { + display: flex; + border-bottom: $border-thin; + background-color: var(--theme-header-background-color); +} + +// Header content contains the components +.header__content { + display: flex; + flex-grow: 1; + max-width: 1400px; // Bootstrap max-width for xl container, to match article + margin: 0 auto; + padding: 0 1rem; + max-height: $header-default-height; + min-height: $header-default-height; + + @media (max-width: $breakpoint-md) { + transition: max-height $animation-time ease-out; + overflow-y: hidden; + padding-bottom: 0.5rem; + } + + // Navigation links + a, + div.dropdown button { + color: var(--theme-header-button-color); + + &:hover { + color: var(--theme-header-button-color-hover); + text-decoration: none; + } + } + + // Button component styling (won't apply to dropdowns) + button.btn-outline-primary { + color: rgba(var(--pst-color-primary), 1); + border-color: rgba(var(--pst-color-primary), 1); + + &:hover { + color: white; + background-color: rgba(var(--pst-color-primary), 1); + } + } +} + +.header__content, +.header__start, +.header__end { + display: flex; + flex-wrap: nowrap; + + // On narrow screens, the header items show flow vertically and snap to left + @media (max-width: $breakpoint-md) { + flex-direction: column; + } +} + +// More rem for the content container +.header__content { + gap: 1rem; +} + +.header__brand, +.header__start, +.header__end { + gap: 0.75rem; + + // On narrow screens the header content sections display vertically + @media (max-width: $breakpoint-md) { + padding-left: 0.5rem; + } +} + +input#__header { + // Inputs never display, only used to control behavior + display: none; + + // On narrow screens, checking the button opens the header + &:checked ~ div.header__content { + @media (max-width: $breakpoint-md) { + max-height: 20em; + } + } +} + +// This is the logo or a bold docs title +.header__brand { + display: flex; + font-size: 1.5em; + + // Align the brand vertically + .header-content-item { + height: $header-default-height; + align-items: center; + } + + // So the brand logo aligns with the toggle button + button { + padding: 0; + } + + // Only show up on narrow screens, and push to the far right + #header-toggle-button { + display: none; + font-size: 1.5rem; // A bit bigger since it's prominent at top + @media (max-width: $breakpoint-md) { + display: flex; + align-items: center; + height: $header-default-height; + margin-left: auto; + margin-right: 0.4rem; + } + } +} + +.header__start, +.header__end { + .icon-button-no-content { + font-size: 1.5rem; + } + + // Increase the text size on mobile + @media (max-width: $breakpoint-md) { + button { + font-size: var(--sbt-font-size-menu-mobile); + } + } +} + +.header__end { + // Header end is right-justified on wide screens + @media (min-width: $breakpoint-md) { + margin-left: auto; + } + + @media (max-width: $breakpoint-md) { + margin-bottom: 1rem; + } +} + +ul#navbar-icon-links { + flex-direction: row; + gap: 0.75rem; +} + +.header-content-item { + display: flex; + align-items: center; + + // Ensure that the items don't break out of their containers + max-height: $header-default-height; + + // Images are capped a bit smaller so they have whitespace in the header + img { + max-height: $header-default-height * 0.7; + } + + // Spacing is horizontal on wide, vertical on narrow + // Items should snap to left on mobile + @media (max-width: $breakpoint-md) { + align-items: start; + } + + // To put this on top of other sticky content on the page + .dropdown-menu { + z-index: $zindex-sticky + 1; + } +} diff --git a/src/sphinx_book_theme/assets/styles/sections/_headers.scss b/src/sphinx_book_theme/assets/styles/sections/_headers.scss deleted file mode 100644 index e8504158..00000000 --- a/src/sphinx_book_theme/assets/styles/sections/_headers.scss +++ /dev/null @@ -1,22 +0,0 @@ -/** - * A few different header CSS rules - */ -.header-item { - width: 100%; - text-align: center; - - &:empty { - display: none; - } - - &.announcement { - background-color: #616161; - color: white; - padding: 0.4em 12.5%; // Horizontal padding so the width is 75% - - @media (max-width: $breakpoint-md) { - // Announcements can take a bit more width on mobile - padding: 0.4em 2%; - } - } -} diff --git a/src/sphinx_book_theme/assets/styles/sections/_sidebar-primary.scss b/src/sphinx_book_theme/assets/styles/sections/_sidebar-primary.scss index 5ea829e3..eaaa5634 100644 --- a/src/sphinx_book_theme/assets/styles/sections/_sidebar-primary.scss +++ b/src/sphinx_book_theme/assets/styles/sections/_sidebar-primary.scss @@ -3,9 +3,9 @@ *********************************************/ #site-navigation { padding-top: 0; - width: $leftbar-width-wide; + width: $sidebar-primary-width-wide; font-size: var(--sbt-sidebar-font-size); - top: 0px !important; + top: 0px; background: white; border-right: $border-thin; transition: margin-left $animation-time ease 0s, @@ -16,9 +16,9 @@ // On mobile, behave like a slide-out drawer @media (max-width: $breakpoint-md) { position: fixed; - width: $leftbar-width-mobile; + width: $sidebar-primary-width-mobile; max-width: 300px; - font-size: 1.2em; + font-size: var(--sbt-font-size-menu-mobile); z-index: $zindex-offcanvas; } @@ -35,7 +35,7 @@ // Apply some basic padding so the dropdown buttons don't overlap w/ scrollbar .bd-sidebar__top, .bd-sidebar__bottom { - padding: 0 1rem 0rem 1.5rem; + padding: $sidebar-primary-padding; } // This should always snap to the bottom even if there's no sidebar content @@ -83,10 +83,6 @@ } div.navbar-brand-box { - @media (min-width: $breakpoint-md) { - padding-top: 2em; - } - a.navbar-brand { width: 100%; height: auto; diff --git a/src/sphinx_book_theme/assets/styles/sections/_sidebar-secondary.scss b/src/sphinx_book_theme/assets/styles/sections/_sidebar-secondary.scss index da6e8fd5..b11cc539 100644 --- a/src/sphinx_book_theme/assets/styles/sections/_sidebar-secondary.scss +++ b/src/sphinx_book_theme/assets/styles/sections/_sidebar-secondary.scss @@ -27,11 +27,11 @@ background-color: white; border-left: $border-thin; - // Fonts are a bit bigger on mobile, and nested headers get smaller - font-size: 1.4em; - + // Fonts and spacing + font-size: var(--sbt-font-size-menu-mobile); li { - font-size: 0.8em; + // This makes the nested list items smaller + font-size: 0.9em; } } diff --git a/src/sphinx_book_theme/assets/styles/sections/_sidebars-toggle.scss b/src/sphinx_book_theme/assets/styles/sections/_sidebars-toggle.scss index 4d1ecbe9..1f24faac 100644 --- a/src/sphinx_book_theme/assets/styles/sections/_sidebars-toggle.scss +++ b/src/sphinx_book_theme/assets/styles/sections/_sidebars-toggle.scss @@ -13,7 +13,7 @@ input#__navigation { & ~ .container-xl #site-navigation { visibility: hidden; opacity: 0; - margin-left: -$leftbar-width-wide; + margin-left: -$sidebar-primary-width-wide; } // When we hide the sidebar on widescreen, add some padding to content @@ -29,7 +29,7 @@ input#__navigation { &:not(:checked) ~ .container-xl #site-navigation { visibility: hidden; opacity: 0; - margin-left: -$leftbar-width-mobile; + margin-left: -$sidebar-primary-width-mobile; } } } @@ -40,7 +40,7 @@ input#__page-toc { &:not(:checked) ~ .container-xl .bd-toc { visibility: hidden; opacity: 0; - margin-right: -$leftbar-width-mobile; + margin-right: -$sidebar-primary-width-mobile; } } } diff --git a/src/sphinx_book_theme/header_buttons/__init__.py b/src/sphinx_book_theme/header_buttons/__init__.py index d272efc1..d9b38a89 100644 --- a/src/sphinx_book_theme/header_buttons/__init__.py +++ b/src/sphinx_book_theme/header_buttons/__init__.py @@ -36,9 +36,9 @@ def add_header_buttons(app, pagename, templatename, context, doctree): if _as_bool(opts.get("use_fullscreen_button", True)): header_buttons.append( { - "type": "javascript", - "javascript": "toggleFullScreen()", - "tooltip": "Fullscreen mode", + "type": "button", + "onclick": "toggleFullScreen()", + "title": "Fullscreen mode", "icon": "fas fa-expand", } ) @@ -65,10 +65,10 @@ def add_header_buttons(app, pagename, templatename, context, doctree): if opts.get("use_repository_button"): repo_buttons.append( { - "type": "link", + "type": "button", "url": repo_url, - "tooltip": "Source repository", - "text": "repository", + "title": "Source repository", + "content": "repository", "icon": "fab fa-github", } ) @@ -76,10 +76,10 @@ def add_header_buttons(app, pagename, templatename, context, doctree): if opts.get("use_issues_button"): repo_buttons.append( { - "type": "link", + "type": "button", "url": f"{repo_url}/issues/new?title=Issue%20on%20page%20%2F{context['pagename']}.html&body=Your%20issue%20content%20here.", # noqa: E501 - "text": "open issue", - "tooltip": "Open an issue", + "content": "open issue", + "title": "Open an issue", "icon": "fas fa-lightbulb", } ) @@ -103,10 +103,10 @@ def add_header_buttons(app, pagename, templatename, context, doctree): repo_buttons.append( { - "type": "link", + "type": "button", "url": context["get_edit_url"](), - "tooltip": "Edit this page", - "text": "suggest edit", + "title": "Edit this page", + "content": "suggest edit", "icon": "fas fa-pencil-alt", } ) @@ -117,16 +117,16 @@ def add_header_buttons(app, pagename, templatename, context, doctree): rb["tooltip_placement"] = "left" header_buttons.append( { - "type": "group", - "tooltip": "Source repositories", + "type": "dropdown", "icon": "fab fa-github", - "buttons": repo_buttons, - "label": "repository-buttons", + "items": repo_buttons, + "side": "right", + "classes": ["repo-buttons"], } ) elif len(repo_buttons) == 1: # Remove the text since it's just a single button, want just an icon. - repo_buttons[0]["text"] = "" + repo_buttons[0]["content"] = "" header_buttons.extend(repo_buttons) # Download buttons for various source content. @@ -137,31 +137,31 @@ def add_header_buttons(app, pagename, templatename, context, doctree): if context.get("ipynb_source"): download_buttons.append( { - "type": "link", + "type": "button", "url": f'{pathto("_sources", 1)}/{context.get("ipynb_source")}', - "text": ".ipynb", + "content": ".ipynb", "icon": "fas fa-code", - "tooltip": "Download notebook file", + "title": "Download notebook file", "tooltip_placement": "left", } ) download_buttons.append( { - "type": "link", + "type": "button", "url": f'{pathto("_sources", 1)}/{context["sourcename"]}', - "text": suff, - "tooltip": "Download source file", + "content": suff, + "title": "Download source file", "tooltip_placement": "left", "icon": "fas fa-file", } ) download_buttons.append( { - "type": "javascript", - "javascript": "printPdf(this)", - "text": ".pdf", - "tooltip": "Print to PDF", + "type": "button", + "onclick": "printPdf(this)", + "content": ".pdf", + "title": "Print to PDF", "tooltip_placement": "left", "icon": "fas fa-file-pdf", } @@ -170,10 +170,9 @@ def add_header_buttons(app, pagename, templatename, context, doctree): # Add the group header_buttons.append( { - "type": "group", - "tooltip": "Download this page", + "type": "dropdown", "icon": "fas fa-download", - "buttons": download_buttons, - "label": "download-buttons", + "items": download_buttons, + "side": "right", } ) diff --git a/src/sphinx_book_theme/header_buttons/_components.py b/src/sphinx_book_theme/header_buttons/_components.py new file mode 100644 index 00000000..a1e6d822 --- /dev/null +++ b/src/sphinx_book_theme/header_buttons/_components.py @@ -0,0 +1,247 @@ +"""Functions to compile HTML components to be placed on a page. + +These are meant to be used by Jinja templates via the Sphinx HTML context. + +A dictionary defines the components that are available to the theme. +Keys of this dictionary should be the `"type"` values that users provide in +their configuration. +The remaining values in the user configuration are passed as kwargs to the func. +""" +from sphinx.util import logging +import hashlib + +SPHINX_LOGGER = logging.getLogger(__name__) + + +def component_button( + app, + context, + content=None, + title=None, + icon=None, + image=None, + outline=None, + id=None, + tooltip_placement=None, + url=None, + onclick=None, + button_id=None, + label_for=None, + attributes={}, + classes=[], +): + """Render a clickable button. + + There are three possible actions that will be triggered, + corresponding to different kwargs having values. + + Meta Parameters + --------------- + app: An instance of sphinx.Application + context: A Sphinx build context dictionary + + General parameters + ------------------ + content: Content to populate inside the button. + title: A tooltip / accessibility-friendly title. + icon: A tiny square icon. A set of FontAwesome icon classes, or path to an image. + image: A larger image of any aspect ratio. A path to a local or remote image. + button_id: The ID to be added to this button. + outline: Whether to outline the button. + tooltip_placement: Whether the tooltip will be to the left, right, top, or bottom. + attributes: A dictionary of any key:val attributes to add to the button. + classes: A list of CSS classes to add to the button. + + Action-specific parameters + -------------------------- + url: The URL to which a button will direct when clicked. + onclick: JavaScript that will be called when a person clicks. + label_for: The input this label should trigger when clicked (button is a label). + """ + # Set up attributes and classes that will be used to create HTML attributes at end + attributes = attributes.copy() + attributes.update({"type": "button"}) + + # Update classes with custom added ones + default_classes = ["btn", "icon-button"] + if classes: + if isinstance(classes, str): + classes = [classes] + else: + classes = [] + classes.extend(default_classes) + + # Give an outline if desired. + if outline: + classes.append("btn-outline") + + # Checks for proper arguments + btn_content = "" + if url and onclick: + raise Exception("Button component cannot have both url and onclick specified.") + + if not (icon or content or image): + raise Exception("Button must have either icon, content, or image specified.") + + if onclick: + attributes["onclick"] = onclick + + if id: + attributes["id"] = id + + if icon: + if icon.startswith("fa"): + icon = f'' + else: + if not icon.startswith("http"): + icon = context["pathto"](icon, 1) + icon = f'' + btn_content += f'{icon}' + + if image: + if not image.startswith("http"): + image = context["pathto"](image, 1) + btn_content += f""" + + """ + + if not content: + classes.append("icon-button-no-content") + else: + btn_content += f'{content}' + + if button_id: + attributes["id"] = button_id + + # Handle tooltips if a title is given + if title: + title = context["translate"](title) + tooltip_placement = "bottom" if not tooltip_placement else tooltip_placement + attributes["data-toggle"] = "tooltip" + attributes["aria-label"] = title + attributes["data-placement"] = tooltip_placement + attributes["title"] = title + + # Convert all the options for the button into a string of HTML attributes + attributes["class"] = " ".join(classes) + attributes_str = " ".join([f'{key}="{val}"' for key, val in attributes.items()]) + + # Generate the button HTML + if label_for: + html = f""" + + """ + else: + html = f""" + + """ + + # Wrap the whole thing in a link if one is specified + if url: + # If it doesn't look like a web URL, assume it's a local page + if not url.startswith("http"): + url = context["pathto"](url) + html = f'{html}' + + return html + + +def component_group(app, context, items=None, **kwargs): + # Items to go inside dropdown + group_items = [] + for component in items: + # Pop the `button` type in case it was incorrectly given, since we force button + if "type" in component: + component.pop("type") + group_items.append( + component_button( + app, + context, + **component, + ) + ) + group_items = "\n".join(group_items) + html = f""" +
{group_items}
+ """ + return html + + +def component_dropdown( + app, context, content="", icon="", side="left", classes=None, items=[], **kwargs +): + # Render the items inside the dropdown + dropdown_items = [] + for component in items: + # Pop the `button` type in case it was incorrectly given, since we force button + if "type" in component: + component.pop("type") + dropdown_items.append( + component_button( + app, + context, + **component, + ) + ) + dropdown_items = "\n".join(dropdown_items) + + # Set up the classes for the dropdown + classes = [] if not classes else classes + if content: + classes.append("dropdown-toggle") + + # Unique ID to trigger the show event + dropdown_id = "menu-dropdown-" + dropdown_id += hashlib.md5(dropdown_items.encode("utf-8")).hexdigest()[:5] + + # Generate the dropdown button HTML + dropdown_attributes = { + "aria-haspopup": "true", + "aria-expanded": "false", + "type": "button", + } + if "title" in kwargs: + SPHINX_LOGGER.warn("Cannot use title / tooltip with dropdown menu. Removing.") + kwargs.pop("title") + + html_button = component_button( + app, + context, + content=content, + icon=icon, + attributes=dropdown_attributes, + classes=classes, + button_id=dropdown_id, + **kwargs, + ) + + dropdown_classes = ["dropdown-menu"] + if side == "right": + dropdown_classes.append("dropdown-menu-right") + dropdown_classes = " ".join(dropdown_classes) + + html_dropdown = f""" + + """ # noqa + return html_dropdown + + +def component_html(app, context, html=""): + return html + + +COMPONENT_FUNCS = { + "button": component_button, + "dropdown": component_dropdown, + "group": component_group, + "html": component_html, +} diff --git a/src/sphinx_book_theme/header_buttons/_launch.py b/src/sphinx_book_theme/header_buttons/_launch.py new file mode 100644 index 00000000..1a2201aa --- /dev/null +++ b/src/sphinx_book_theme/header_buttons/_launch.py @@ -0,0 +1,322 @@ +from pathlib import Path +from typing import Any, Dict, Optional +from urllib.parse import urlencode + +from docutils.nodes import document +from sphinx.application import Sphinx +from sphinx.util import logging +from shutil import copy2 + + +SPHINX_LOGGER = logging.getLogger(__name__) + + +def add_launch_buttons( + app: Sphinx, + pagename: str, + templatename: str, + context: Dict[str, Any], + doctree: Optional[document], +): + """Builds a binder link and inserts it in HTML context for use in templating. + + This is a ``html-page-context`` sphinx event (see :ref:`sphinx:events`). + + :param pagename: The sphinx docname related to the page + :param context: A dictionary of values that are given to the template engine, + to render the page and can be modified to include custom values. + :param doctree: A doctree when the page is created from a reST documents; + it will be None when the page is created from an HTML template alone. + + """ + + # We only need to run this if the page is a notebook + config_theme = app.config["html_theme_options"] + launch_buttons = config_theme.get("launch_buttons", {}) + if not launch_buttons or not _is_notebook(app, pagename): + return + + # If we have a Jupytext markdown notebook + # Check whether an .ipynb version exists and add a link if so + if context["sourcename"].endswith(".md") or context["sourcename"].endswith( + ".md.txt" + ): + # Figure out the folders we want + out_dir = Path(app.outdir) + build_dir = out_dir.parent + ntbk_dir = build_dir.joinpath("jupyter_execute") + sources_dir = out_dir.joinpath("_sources") + # Paths to old and new notebooks + path_ntbk = ntbk_dir.joinpath(pagename).with_suffix(".ipynb") + path_new_notebook = sources_dir.joinpath(pagename).with_suffix(".ipynb") + # Copy the notebook to `_sources` dir so it can be downloaded + path_new_notebook.parent.mkdir(exist_ok=True, parents=True) + copy2(path_ntbk, path_new_notebook) + context["ipynb_source"] = pagename + ".ipynb" + + # Check if we have a non-ipynb file, but an ipynb of same name exists + # If so, we'll use the ipynb extension instead of the text extension + path = app.env.doc2path(pagename) + extension = Path(path).suffix + if extension != ".ipynb" and Path(path).with_suffix(".ipynb").exists(): + extension = ".ipynb" + + # Theme-level configuration for repository location + repo_parts = _get_repo_parts(config_theme) + # Check for missing branch and default to main + if not repo_parts.get("branch"): + repo_parts["branch"] = "main" + + # Path to the source file relative to the repository root + book_relpath = config_theme.get("path_to_docs", "").strip("/") + if book_relpath != "": + book_relpath += "/" + path_rel_repo = f"{book_relpath}{pagename}{extension}" + + # Iterate through launch buttons and generate button config for them + launch_buttons_configs = [] + for button in launch_buttons: + if button.get("type") == "binderhub": + hub_url = button.get("hub_url") + if not hub_url: + raise ValueError(f"No `hub_url` given: {button}") + interface = button.get("notebook_interface", "classic") + + # This will only update keys if they've been given + repo_parts.update(_get_repo_parts(button)) + repo_url, org, repo, branch = _check_repo_parts(repo_parts) + + ui_pre = _get_notebook_interface_prefix(interface) + url = ( + f"{hub_url}/v2/gh/{org}/{repo}/{branch}?" + f"urlpath={ui_pre}/{path_rel_repo}" + ) + launch_buttons_configs.append( + { + "type": "button", + "content": button.get("content", "Binder"), + "title": button.get("title", "Launch on Binder"), + "icon": "_static/images/logo_binder.svg", + "url": url, + } + ) + + if button.get("type") == "jupyterhub": + hub_url = button.get("hub_url") + if not hub_url: + raise ValueError(f"No `hub_url` given: {button}") + interface = button.get("notebook_interface", "classic") + ui_pre = _get_notebook_interface_prefix(interface) + + # This will only update keys if they've been given + repo_parts.update(_get_repo_parts(button)) + repo_url, org, repo, branch = _check_repo_parts(repo_parts) + + url_params = urlencode( + dict( + repo=repo_url, + urlpath=f"{ui_pre}/{repo}/{path_rel_repo}", + branch=branch, + ), + safe="/", + ) + url = f"{hub_url}/hub/user-redirect/git-pull?{url_params}" + launch_buttons_configs.append( + { + "type": "button", + "content": button.get("content", "JupyterHub"), + "title": button.get("title", "Launch on JupyterHub"), + "icon": "_static/images/logo_jupyterhub.svg", + "url": url, + } + ) + + if button.get("type") == "colab": + url = f"https://colab.research.google.com/github/{org}/{repo}/blob/{branch}/{path_rel_repo}" # noqa + launch_buttons_configs.append( + { + "type": "button", + "content": "Colab", + "title": "Launch on Colab", + "icon": "_static/images/logo_colab.png", + "url": url, + } + ) + + if button.get("type") == "deepnote": + github_path = f"%2F{org}%2F{repo}%2Fblob%2F{branch}%2F{path_rel_repo}" + url = ( + f"https://deepnote.com/launch?url=https%3A%2F%2Fgithub.com{github_path}" + ) + launch_buttons_configs.append( + { + "type": "button", + "content": "Deepnote", + "title": "Launch on Deepnote", + "icon": "_static/images/logo_deepnote.svg", + "url": url, + } + ) + + if button.get("type") == "thebe": + # Note that the thebe config will have already been initialized. + launch_buttons_configs.append( + { + "type": "button", + "content": "Interactive code", + "title": "Interactive code", + "onclick": "initThebeSBT()", + "icon": "fas fa-play", + } + ) + context["use_thebe"] = True + + # Add the dropdown to our header buttons list + for lb in launch_buttons_configs: + lb["tooltip_placement"] = "left" + + context["header_buttons"].append( + { + "type": "dropdown", + "icon": "fas fa-rocket", + "side": "right", + "classes": ["launch-buttons"], + "items": launch_buttons_configs, + } + ) + + +def _split_repo_url(url): + """Split a repository URL into an org / repo combination.""" + if "github.com/" in url: + end = url.split("github.com/")[-1] + org, repo = end.split("/")[:2] + else: + SPHINX_LOGGER.warning( + f"Currently Binder/JupyterHub repositories must be on GitHub, got {url}" + ) + org = repo = None + return org, repo + + +def _get_repo_parts(config): + """Return metadata about a repository based on a configuration file. + + Used to generate launch button URLs. + """ + # Use an empty config so we only add key/vals if the val exists + repo_parts = {} + + # Repository URL is used to infer the org/repo + repo_url = config.get("repository_url") + + # Check to make sure that the URL is properly formed + if repo_url: + repo_parts["url"] = repo_url + org, repo = _split_repo_url(repo_url) + if org is None or repo is None: + # Skip the rest because the repo_url isn't right + SPHINX_LOGGER.warn(f"Repository URL {repo_url} is not properly structured.") + return + repo_parts["org"] = org + repo_parts["repo"] = repo + + # The branch is given separately + branch = config.get("repository_branch") + if branch: + repo_parts["branch"] = branch + + return repo_parts + + +def _is_notebook(app, pagename): + return app.env.metadata[pagename].get("kernelspec") + + +def _check_repo_parts(parts): + """Check that all parts of a launch button URL are present.""" + if not parts.get("url"): + raise ValueError( + "You must provide the key: `repository_url` to use launch buttons." + ) + elif not parts.get("org") or not parts.get("repo"): + raise ValueError( + ( + "Couldn't infer an org / repo from the repo url. " + f"Check it is correct: {parts}" + ) + ) + elif not parts.get("branch"): + raise ValueError("No branch specified for launch buttons.") + + return parts.get("url"), parts.get("org"), parts.get("repo"), parts.get("branch") + + +def _get_notebook_interface_prefix(interface): + """Generate the correct URL prefix for a Binder/Hub URL given an interface.""" + # Construct the extra URL parts (app and relative path) + notebook_interface_prefixes = {"classic": "tree", "jupyterlab": "lab/tree"} + if interface not in notebook_interface_prefixes: + raise ValueError( + ( + "Notebook UI for Binder/JupyterHub links must be one" + f"of {tuple(notebook_interface_prefixes.keys())}," + f"not {interface}" + ) + ) + ui_pre = notebook_interface_prefixes[interface] + return ui_pre + + +def update_launch_button_config(app): + """If a thebe launch button is specified, activate thebe and add configuration.""" + theme_options = app.env.config.html_theme_options + launch_buttons = theme_options.get("launch_buttons", []) + + # DEPRECATE after 0.5 + # Old versions had people give dictionary to configure launch buttons + # New versions use a list of launch button configuration + # So we check for the old-style dictionary and convert it to new style + if isinstance(launch_buttons, dict): + SPHINX_LOGGER.warn( + "Launch buttons are now configured with a list of buttons, rather than a dictionary. Dictionary config will be deprecated in v0.5" # noqa + ) + launch_buttons_new = [] + for kind, url in launch_buttons.items(): + if kind == "thebe": + url = theme_options.get("repository_url") + + # Convert the type:url dict into a generic key:val dict we use later + launch_buttons_new.append( + {"type": kind.split("_")[0], "url": url.strip("/")} + ) + theme_options["launch_buttons"] = launch_buttons = launch_buttons_new + + # If any of our launch buttons adds thebe, configure sphinx-thebe here + for button in launch_buttons: + # If the button isn't a thebe type, we have nothing to do + if not button["type"] == "thebe": + continue + + # Make sure the sphinx-thebe extension is activated + app.setup_extension("sphinx_thebe") + + # This is either the default values or will have config if the user defined it + thebe_config = app.env.config.thebe_config + + # Update the thebe config with values given in theme options or button config + thebe_config["repository_url"] = theme_options.get("repository_url") + if button.get("repository_url"): + thebe_config["repository_url"] = button.get("repository_url") + + branch = theme_options.get("repository_url") + if button.get("repository_branch"): + branch = button.get("repository_branch") + if not branch: + # Explicitly check in case branch is "" + SPHINX_LOGGER.warn("No thebe branch specified. Using 'main'.") + branch = "main" + thebe_config["repository_branch"] = branch + + # Update the thebe_config with the new values + app.env.config.thebe_config = thebe_config diff --git a/src/sphinx_book_theme/header_buttons/launch.py b/src/sphinx_book_theme/header_buttons/launch.py deleted file mode 100644 index c5b220b0..00000000 --- a/src/sphinx_book_theme/header_buttons/launch.py +++ /dev/null @@ -1,222 +0,0 @@ -from pathlib import Path -from typing import Any, Dict, Optional -from urllib.parse import urlencode - -from docutils.nodes import document -from sphinx.application import Sphinx -from sphinx.util import logging -from shutil import copy2 - - -SPHINX_LOGGER = logging.getLogger(__name__) - - -def add_launch_buttons( - app: Sphinx, - pagename: str, - templatename: str, - context: Dict[str, Any], - doctree: Optional[document], -): - """Builds a binder link and inserts it in HTML context for use in templating. - - This is a ``html-page-context`` sphinx event (see :ref:`sphinx:events`). - - :param pagename: The sphinx docname related to the page - :param context: A dictionary of values that are given to the template engine, - to render the page and can be modified to include custom values. - :param doctree: A doctree when the page is created from a reST documents; - it will be None when the page is created from an HTML template alone. - - """ - - # First decide if we'll insert any links - path = app.env.doc2path(pagename) - extension = Path(path).suffix - - # If so, insert the URLs depending on the configuration - config_theme = app.config["html_theme_options"] - launch_buttons = config_theme.get("launch_buttons", {}) - if not launch_buttons or not _is_notebook(app, pagename): - return - - # Grab the header buttons from context as it should already exist. - header_buttons = context["header_buttons"] - - # Check if we have a markdown notebook, and if so then add a link to the context - if _is_notebook(app, pagename) and ( - context["sourcename"].endswith(".md") - or context["sourcename"].endswith(".md.txt") - ): - # Figure out the folders we want - out_dir = Path(app.outdir) - build_dir = out_dir.parent - ntbk_dir = build_dir.joinpath("jupyter_execute") - sources_dir = out_dir.joinpath("_sources") - # Paths to old and new notebooks - path_ntbk = ntbk_dir.joinpath(pagename).with_suffix(".ipynb") - path_new_notebook = sources_dir.joinpath(pagename).with_suffix(".ipynb") - # Copy the notebook to `_sources` dir so it can be downloaded - path_new_notebook.parent.mkdir(exist_ok=True, parents=True) - copy2(path_ntbk, path_new_notebook) - context["ipynb_source"] = pagename + ".ipynb" - - repo_url = _get_repo_url(config_theme) - - # Parse the repo parts from the URL - org, repo = _split_repo_url(repo_url) - if org is None and repo is None: - # Skip the rest because the repo_url isn't right - return - - branch = _get_branch(config_theme) - - # Construct the extra URL parts (app and relative path) - notebook_interface_prefixes = {"classic": "tree", "jupyterlab": "lab/tree"} - notebook_interface = launch_buttons.get("notebook_interface", "classic") - if notebook_interface not in notebook_interface_prefixes: - raise ValueError( - ( - "Notebook UI for Binder/JupyterHub links must be one" - f"of {tuple(notebook_interface_prefixes.keys())}," - f"not {notebook_interface}" - ) - ) - ui_pre = notebook_interface_prefixes[notebook_interface] - - # Check if we have a non-ipynb file, but an ipynb of same name exists - # If so, we'll use the ipynb extension instead of the text extension - if extension != ".ipynb" and Path(path).with_suffix(".ipynb").exists(): - extension = ".ipynb" - - # Construct a path to the file relative to the repository root - book_relpath = config_theme.get("path_to_docs", "").strip("/") - if book_relpath != "": - book_relpath += "/" - path_rel_repo = f"{book_relpath}{pagename}{extension}" - - # Container for launch buttons - launch_buttons_list = [] - - # Now build infrastructure-specific links - jupyterhub_url = launch_buttons.get("jupyterhub_url", "").strip("/") - binderhub_url = launch_buttons.get("binderhub_url", "").strip("/") - colab_url = launch_buttons.get("colab_url", "").strip("/") - deepnote_url = launch_buttons.get("deepnote_url", "").strip("/") - if binderhub_url: - url = ( - f"{binderhub_url}/v2/gh/{org}/{repo}/{branch}?" - f"urlpath={ui_pre}/{path_rel_repo}" - ) - launch_buttons_list.append( - { - "type": "link", - "text": "Binder", - "tooltip": "Launch on Binder", - "icon": "_static/images/logo_binder.svg", - "url": url, - } - ) - - if jupyterhub_url: - url_params = urlencode( - dict( - repo=repo_url, urlpath=f"{ui_pre}/{repo}/{path_rel_repo}", branch=branch - ), - safe="/", - ) - url = f"{jupyterhub_url}/hub/user-redirect/git-pull?{url_params}" - launch_buttons_list.append( - { - "type": "link", - "text": "JupyterHub", - "tooltip": "Launch on JupyterHub", - "icon": "_static/images/logo_jupyterhub.svg", - "url": url, - } - ) - - if colab_url: - url = f"{colab_url}/github/{org}/{repo}/blob/{branch}/{path_rel_repo}" - launch_buttons_list.append( - { - "type": "link", - "text": "Colab", - "tooltip": "Launch on Colab", - "icon": "_static/images/logo_colab.png", - "url": url, - } - ) - - if deepnote_url: - github_path = f"%2F{org}%2F{repo}%2Fblob%2F{branch}%2F{path_rel_repo}" - url = f"{deepnote_url}/launch?url=https%3A%2F%2Fgithub.com{github_path}" - launch_buttons_list.append( - { - "type": "link", - "text": "Deepnote", - "tooltip": "Launch on Deepnote", - "icon": "_static/images/logo_deepnote.svg", - "url": url, - } - ) - - # Add thebe flag in context - if launch_buttons.get("thebe", False): - launch_buttons_list.append( - { - "type": "javascript", - "text": "Live Code", - "tooltip": "Launch Thebe", - "javascript": "initThebeSBT()", - "icon": "fas fa-play", - "label": "launch-thebe", - } - ) - context["use_thebe"] = True - - # Add the buttons to header_buttons - for lb in launch_buttons_list: - lb["tooltip_placement"] = "left" - header_buttons.append( - { - "type": "group", - "tooltip": "Launch interactive content", - "icon": "fas fa-rocket", - "buttons": launch_buttons_list, - "label": "launch-buttons", - } - ) - - -def _split_repo_url(url): - """Split a repository URL into an org / repo combination.""" - if "github.com/" in url: - end = url.split("github.com/")[-1] - org, repo = end.split("/")[:2] - else: - SPHINX_LOGGER.warning( - f"Currently Binder/JupyterHub repositories must be on GitHub, got {url}" - ) - org = repo = None - return org, repo - - -def _get_repo_url(config): - repo_url = config.get("repository_url") - if not repo_url: - raise ValueError( - "You must provide the key: `repository_url` to use launch buttons." - ) - return repo_url - - -def _is_notebook(app, pagename): - return app.env.metadata[pagename].get("kernelspec") - - -def _get_branch(config_theme): - branch = config_theme.get("repository_branch") - if not branch: - branch = "master" - return branch diff --git a/src/sphinx_book_theme/margin/__init__.py b/src/sphinx_book_theme/margin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/sphinx_book_theme/_transforms.py b/src/sphinx_book_theme/margin/_transforms.py similarity index 100% rename from src/sphinx_book_theme/_transforms.py rename to src/sphinx_book_theme/margin/_transforms.py index 1b5dc2d1..6d73721a 100644 --- a/src/sphinx_book_theme/_transforms.py +++ b/src/sphinx_book_theme/margin/_transforms.py @@ -2,8 +2,8 @@ from typing import Any from docutils import nodes as docutil_nodes from sphinx import addnodes as sphinx_nodes -from .nodes import SideNoteNode import copy +from .nodes import SideNoteNode class HandleFootnoteTransform(SphinxPostTransform): diff --git a/src/sphinx_book_theme/nodes.py b/src/sphinx_book_theme/margin/nodes.py similarity index 100% rename from src/sphinx_book_theme/nodes.py rename to src/sphinx_book_theme/margin/nodes.py diff --git a/src/sphinx_book_theme/theme/sphinx_book_theme/layout.html b/src/sphinx_book_theme/theme/sphinx_book_theme/layout.html index 37f19bce..af0588e8 100644 --- a/src/sphinx_book_theme/theme/sphinx_book_theme/layout.html +++ b/src/sphinx_book_theme/theme/sphinx_book_theme/layout.html @@ -19,7 +19,7 @@
{%- include "sections/announcement.html" -%}
-
+
{%- include "sections/header.html" -%}
{{ super() }} diff --git a/src/sphinx_book_theme/theme/sphinx_book_theme/macros/buttons.html b/src/sphinx_book_theme/theme/sphinx_book_theme/macros/buttons.html deleted file mode 100644 index 729ab074..00000000 --- a/src/sphinx_book_theme/theme/sphinx_book_theme/macros/buttons.html +++ /dev/null @@ -1,78 +0,0 @@ -{# Utility macros we'll re-use below -#} -{% macro render_tooltip_metadata(tooltip, tooltip_placement) -%} -data-toggle="tooltip" -data-placement="{{ tooltip_placement }}" -title="{{ translate(tooltip) }}" -{%- endmacro %} - - -{% macro render_inner_html(icon, text) %} -{# used across multiple button types #} -{% if icon -%} - - {% if icon.startswith("fa") -%} - - {% else %} - - {% endif -%} - -{% endif %} -{%- if text %}{{ translate(text) }}{% endif -%} -{% endmacro %} - - -{% macro render_link_button(url, tooltip=None, icon=None, text=None, label=None, tooltip_placement="bottom") -%} - - {{ render_inner_html(icon, text) }} - -{% endmacro %} - - -{% macro render_js_button(javascript, tooltip=None, icon=None, text=None, label=None, tooltip_placement="bottom") %} - -{% endmacro %} - - -{% macro render_label_input_button(for_input, tooltip=None, icon=None, text=None, label=None, tooltip_placement="bottom") -%} - -{% endmacro %} - - -{% macro render_button_group(buttons, icon, tooltip=None, label=None) %} - -{% endmacro %} - -{%- set render_funcs = { - "group" : render_button_group, - "javascript" : render_js_button, - "link": render_link_button, - "input_label": render_label_input_button, -} --%} diff --git a/src/sphinx_book_theme/theme/sphinx_book_theme/sections/header-article.html b/src/sphinx_book_theme/theme/sphinx_book_theme/sections/header-article.html index 318edb89..e67219df 100644 --- a/src/sphinx_book_theme/theme/sphinx_book_theme/sections/header-article.html +++ b/src/sphinx_book_theme/theme/sphinx_book_theme/sections/header-article.html @@ -1,20 +1,19 @@ {# To trigger whether the TOC and its button show up #} {% set page_toc = generate_toc_html() %} -{% from "../macros/buttons.html" import render_funcs, render_label_input_button with context %}
{% if theme_single_page != True %} - {{ render_label_input_button(for_input="__navigation", tooltip="Toggle navigation", icon="fas fa-bars", tooltip_placement="right") }} + {{ theme_render_component({"type": "button", "title": "Toggle navigation", "label_for": "__navigation", "icon": "fas fa-bars"}) }} {% endif %}
{%- for button in header_buttons -%} - {{ render_funcs[button.pop("type")](**button) }} + {{ theme_render_component(button) }} {%- endfor -%} {% if page_toc -%} - {{ render_label_input_button("__page-toc", icon="fas fa-list", label="page-toc") }} + {{ theme_render_component({"type": "button", "title": "Toggle page Table of Contents", "id": "header__page-toc-button", "label_for": "__page-toc", "icon": "fas fa-list"}) }} {%- endif %}
diff --git a/src/sphinx_book_theme/theme/sphinx_book_theme/sections/header.html b/src/sphinx_book_theme/theme/sphinx_book_theme/sections/header.html index e69de29b..534b3348 100644 --- a/src/sphinx_book_theme/theme/sphinx_book_theme/sections/header.html +++ b/src/sphinx_book_theme/theme/sphinx_book_theme/sections/header.html @@ -0,0 +1,45 @@ +{% if theme_header %} +{% set start_items = theme_header.get("start", {}) %} +{% set end_items = theme_header.get("end", {}) %} + +{%- if start_items or middle_items or end_items %} + +
+ +
+ {% if theme_header.get("brand") %} +
+ {{ theme_render_component(theme_header.get("brand")) }} +
+ {% endif %} + + + {{ theme_render_component({ + "type": "button", + "label_for": "__header", + "icon": "fas fa-bars", + "title": "Toggle Header", + "id": "header-toggle-button" + }) }} + {% endif -%} +
+ + +
+ {% for item in start_items %} +
+ {{ theme_render_component(item) }} +
+ {% endfor %} +
+ + +
+ {% for item in end_items %} +
+ {{ theme_render_component(item) }} +
+ {% endfor %} +
+
+{% endif %} diff --git a/src/sphinx_book_theme/theme/sphinx_book_theme/theme.conf b/src/sphinx_book_theme/theme/sphinx_book_theme/theme.conf index 122d36ba..66025eae 100644 --- a/src/sphinx_book_theme/theme/sphinx_book_theme/theme.conf +++ b/src/sphinx_book_theme/theme/sphinx_book_theme/theme.conf @@ -14,6 +14,7 @@ repository_url = repository_branch = launch_buttons = {} home_page_in_toc = False +header = logo_only = # DEPRECATE after a few release cycles navbar_footer_text = diff --git a/tests/test_build.py b/tests/test_build.py index 6accf30f..19800e85 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from shutil import copytree, rmtree +from shutil import copytree from subprocess import check_call from bs4 import BeautifulSoup @@ -94,7 +94,7 @@ def test_build_book(sphinx_build_factory, file_regression): sidebar = index_html.find(attrs={"id": "bd-docs-nav"}) file_regression.check( sidebar.prettify(), - basename="build__sidebar-primary__nav", + basename="sidebar-primary__nav", extension=".html", encoding="utf8", ) @@ -114,7 +114,7 @@ def test_build_book(sphinx_build_factory, file_regression): file_regression.check( header_article.prettify(), - basename="build__header-article", + basename="header-article", extension=".html", encoding="utf8", ) @@ -130,7 +130,7 @@ def test_build_book(sphinx_build_factory, file_regression): page_toc = page_html.find("div", attrs={"class": "bd-toc"}) file_regression.check( page_toc.prettify(), - basename=f"build__pagetoc--{page.with_suffix('').name}", + basename=f"sidebar-secondary--{page.with_suffix('').name}", extension=".html", encoding="utf8", ) @@ -188,7 +188,7 @@ def test_navbar_options(sphinx_build_factory, option, value): (False, False, False, "all-off"), ], ) -def test_header_repository_buttons( +def test_header_article_repository_buttons( sphinx_build_factory, file_regression, edit, repo, issues, name ): # All buttons on @@ -209,19 +209,24 @@ def test_header_repository_buttons( ) file_regression.check( header[0].prettify(), - basename=f"header__repo-buttons--{name}", + basename=f"header-article__repo-buttons--{name}", extension=".html", encoding="utf8", ) -def test_header_launchbtns(sphinx_build_factory, file_regression): +def test_header_article_launchbtns(sphinx_build_factory, file_regression): """Test launch buttons.""" sphinx_build = sphinx_build_factory("base").build(assert_pass=True) launch_btns = sphinx_build.html_tree("section1", "ntbk.html").select( - ".menu-dropdown-launch-buttons" + ".launch-buttons + .dropdown-menu" + ) + file_regression.check( + launch_btns[0].prettify(), + basename="header-article__launch-buttons", + extension=".html", + encoding="utf8", ) - file_regression.check(launch_btns[0].prettify(), extension=".html", encoding="utf8") def test_repo_custombranch(sphinx_build_factory, file_regression): @@ -243,7 +248,7 @@ def test_repo_custombranch(sphinx_build_factory, file_regression): # The Binder link should point to `foo`, as should the `edit` button file_regression.check( header[0].prettify(), - basename="header__repo-buttons--custom-branch", + basename="header-article__repo-buttons--custom-branch", extension=".html", encoding="utf8", ) @@ -283,7 +288,7 @@ def test_show_navbar_depth(sphinx_build_factory): assert "checked" not in checkbox.attrs -def test_header_download_button_off(sphinx_build_factory): +def test_header_article_download_button_off(sphinx_build_factory): """Download button should not show up in the header when configured as False.""" confoverrides = { "html_theme_options.use_download_button": False, @@ -320,13 +325,12 @@ def test_right_sidebar_title(sphinx_build_factory, file_regression): sidebar_title = sphinx_build.html_tree("page1.html").find_all( "div", attrs={"class": "tocsection"} )[0] - - file_regression.check(sidebar_title.prettify(), extension=".html", encoding="utf8") - - # Testing the exception for empty title - rmtree(str(sphinx_build.src)) - - confoverrides = {"html_theme_options.toc_title": ""} + file_regression.check( + sidebar_title.prettify(), + basename="sidebar-secondary__toc--customtitle", + extension=".html", + encoding="utf8", + ) def test_logo_only(sphinx_build_factory): @@ -351,7 +355,10 @@ def test_sidenote(sphinx_build_factory, file_regression): sidenote_html = page2.select("section > #sidenotes") file_regression.check( - sidenote_html[0].prettify(), extension=".html", encoding="utf8" + sidenote_html[0].prettify(), + basename="article__margin--sidenote", + extension=".html", + encoding="utf8", ) @@ -365,5 +372,70 @@ def test_marginnote(sphinx_build_factory, file_regression): marginnote_html = page2.select("section > #marginnotes") file_regression.check( - marginnote_html[0].prettify(), extension=".html", encoding="utf8" + marginnote_html[0].prettify(), + basename="article__margin--marginnote", + extension=".html", + encoding="utf8", + ) + + +def test_header(sphinx_build_factory, file_regression): + header_config = { + "logo": { + "type": "button", + "image": "https://executablebooks.org/en/latest/_static/logo.svg", + "url": "https://sphinx-book-theme.readthedocs.io", + }, + # Split our component tests between start and end semi-randomly + "start": [ + # Dropdown button + { + "type": "dropdown", + "classes": ["one", "two"], + "items": [ + { + "type": "link", + "url": "https://google.com", + "content": "Test content 1", + }, + { + "type": "link", + "url": "https://google.com/two", + "icon": "fas fa-bars", + "content": "Test content 2", + }, + ], + }, + # JS button + {"type": "button", "onclick": "somejs()", "content": "Some content"}, + ], + "end": [ + # Link image + {"type": "button", "url": "https://google.com", "image": "noimage"}, + # Icon image (FA) + {"type": "button", "url": "https://google.com", "icon": "fas fa-bars"}, + # Icon image (local) + {"type": "button", "url": "https://google.com", "icon": "hi/there.png"}, + # Local image + {"type": "button", "url": "https://google.com", "image": "hi/there.png"}, + # Remote image + { + "type": "button", + "url": "https://google.com", + "icon": "https://google.com/hi/there.png", + }, + ], + } + confoverrides = {"html_theme_options.header": header_config} + sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build( + assert_pass=True + ) + + index = sphinx_build.html_tree("index.html") + header_html = index.select(".header__content")[0] + file_regression.check( + header_html.prettify(), + basename="header", + extension=".html", + encoding="utf8", ) diff --git a/tests/test_build/test_marginnote.html b/tests/test_build/article__margin--marginnote.html similarity index 100% rename from tests/test_build/test_marginnote.html rename to tests/test_build/article__margin--marginnote.html diff --git a/tests/test_build/test_sidenote.html b/tests/test_build/article__margin--sidenote.html similarity index 100% rename from tests/test_build/test_sidenote.html rename to tests/test_build/article__margin--sidenote.html diff --git a/tests/test_build/build__header-article.html b/tests/test_build/build__header-article.html deleted file mode 100644 index 74bc3326..00000000 --- a/tests/test_build/build__header-article.html +++ /dev/null @@ -1,116 +0,0 @@ -
-
-
- -
-
- - - -
-
- -
-
-
diff --git a/tests/test_build/build__pagetoc--page-onetitlenoheadings.html b/tests/test_build/build__pagetoc--page-onetitlenoheadings.html deleted file mode 100644 index ec08e7c4..00000000 --- a/tests/test_build/build__pagetoc--page-onetitlenoheadings.html +++ /dev/null @@ -1,2 +0,0 @@ -
-
diff --git a/tests/test_build/header-article.html b/tests/test_build/header-article.html new file mode 100644 index 00000000..d0161dee --- /dev/null +++ b/tests/test_build/header-article.html @@ -0,0 +1,112 @@ +
+
+
+ +
+
+ + + +
+
+ +
+
+
diff --git a/tests/test_build/header-article__launch-buttons.html b/tests/test_build/header-article__launch-buttons.html new file mode 100644 index 00000000..4572d60f --- /dev/null +++ b/tests/test_build/header-article__launch-buttons.html @@ -0,0 +1,51 @@ + diff --git a/tests/test_build/header-article__repo-buttons--all-off.html b/tests/test_build/header-article__repo-buttons--all-off.html new file mode 100644 index 00000000..98036f4d --- /dev/null +++ b/tests/test_build/header-article__repo-buttons--all-off.html @@ -0,0 +1,38 @@ +
+ + +
diff --git a/tests/test_build/header-article__repo-buttons--all-on.html b/tests/test_build/header-article__repo-buttons--all-on.html new file mode 100644 index 00000000..53912fb2 --- /dev/null +++ b/tests/test_build/header-article__repo-buttons--all-on.html @@ -0,0 +1,81 @@ +
+ + + +
diff --git a/tests/test_build/header-article__repo-buttons--custom-branch.html b/tests/test_build/header-article__repo-buttons--custom-branch.html new file mode 100644 index 00000000..7e7e21c1 --- /dev/null +++ b/tests/test_build/header-article__repo-buttons--custom-branch.html @@ -0,0 +1,66 @@ +
+ + + + + + +
diff --git a/tests/test_build/header-article__repo-buttons--one-on.html b/tests/test_build/header-article__repo-buttons--one-on.html new file mode 100644 index 00000000..7e941441 --- /dev/null +++ b/tests/test_build/header-article__repo-buttons--one-on.html @@ -0,0 +1,46 @@ +
+ + + + + +
diff --git a/tests/test_build/header__repo-buttons--all-off.html b/tests/test_build/header__repo-buttons--all-off.html deleted file mode 100644 index 0d1f51e2..00000000 --- a/tests/test_build/header__repo-buttons--all-off.html +++ /dev/null @@ -1,40 +0,0 @@ -
- - -
diff --git a/tests/test_build/header__repo-buttons--all-on.html b/tests/test_build/header__repo-buttons--all-on.html deleted file mode 100644 index 2dbc8d5d..00000000 --- a/tests/test_build/header__repo-buttons--all-on.html +++ /dev/null @@ -1,83 +0,0 @@ -
- - - -
diff --git a/tests/test_build/header__repo-buttons--custom-branch.html b/tests/test_build/header__repo-buttons--custom-branch.html deleted file mode 100644 index 1a4f37c5..00000000 --- a/tests/test_build/header__repo-buttons--custom-branch.html +++ /dev/null @@ -1,66 +0,0 @@ -
- - - - - - - - - -
diff --git a/tests/test_build/header__repo-buttons--one-on.html b/tests/test_build/header__repo-buttons--one-on.html deleted file mode 100644 index 63c3c4c8..00000000 --- a/tests/test_build/header__repo-buttons--one-on.html +++ /dev/null @@ -1,46 +0,0 @@ -
- - - - - - - - -
diff --git a/tests/test_build/build__sidebar-primary__nav.html b/tests/test_build/sidebar-primary__nav.html similarity index 100% rename from tests/test_build/build__sidebar-primary__nav.html rename to tests/test_build/sidebar-primary__nav.html diff --git a/tests/test_build/build__pagetoc--page-multipletitles.html b/tests/test_build/sidebar-secondary--page-multipletitles.html similarity index 100% rename from tests/test_build/build__pagetoc--page-multipletitles.html rename to tests/test_build/sidebar-secondary--page-multipletitles.html diff --git a/tests/test_build/build__pagetoc--page-onetitle.html b/tests/test_build/sidebar-secondary--page-onetitle.html similarity index 100% rename from tests/test_build/build__pagetoc--page-onetitle.html rename to tests/test_build/sidebar-secondary--page-onetitle.html diff --git a/tests/test_build/test_right_sidebar_title.html b/tests/test_build/sidebar-secondary__toc--customtitle.html similarity index 100% rename from tests/test_build/test_right_sidebar_title.html rename to tests/test_build/sidebar-secondary__toc--customtitle.html diff --git a/tests/test_build/test_header_launchbtns.html b/tests/test_build/test_header_launchbtns.html deleted file mode 100644 index 6ab4a501..00000000 --- a/tests/test_build/test_header_launchbtns.html +++ /dev/null @@ -1,61 +0,0 @@ - diff --git a/tests/test_build/test_topbar_launchbtns.html b/tests/test_build/test_topbar_launchbtns.html deleted file mode 100644 index 6ab4a501..00000000 --- a/tests/test_build/test_topbar_launchbtns.html +++ /dev/null @@ -1,61 +0,0 @@ -