diff --git a/docs/_static/ebp-logo.png b/docs/_static/ebp-logo.png new file mode 100644 index 00000000..191a9ee8 Binary files /dev/null and b/docs/_static/ebp-logo.png differ diff --git a/docs/components/icon-links.md b/docs/components/icon-links.md new file mode 100644 index 00000000..d91dd4a8 --- /dev/null +++ b/docs/components/icon-links.md @@ -0,0 +1,5 @@ +# Icon links and badges + +You can add a collection of icon links and badges to your primary sidebar. +For example, to include links to your GitHub repository or a [shields.io badge](https://shields.io). +See the {external:ref}`PyData Icon Links documentation ` for how to configure this. diff --git a/docs/components/index.md b/docs/components/index.md index b9bdee3f..92369df8 100644 --- a/docs/components/index.md +++ b/docs/components/index.md @@ -4,6 +4,7 @@ Components are specific UI elements that you can control with configuration. ```{toctree} logo +icon-links download source-files custom-css diff --git a/docs/components/source-files.md b/docs/components/source-files.md index 2c0958b7..677717bd 100644 --- a/docs/components/source-files.md +++ b/docs/components/source-files.md @@ -3,78 +3,123 @@ There are a collection of buttons that you can use to link back to your source repository. This lets users browse the repository, or take actions like suggest -an edit or open an issue. In each case, they require the following configuration -to exist: +an edit or open an issue. + +(source-buttons:repository)= +## Set your source repository + +You need to define a **source repository** for this functionality to work. +This is the online space where your code / documentation is hosted. +To +In each case, they require the following configuration to exist: ```python html_theme_options = { ... - "repository_url": "https://github.com/{your-docs-url}", + "repository_url": "https://{your-provider}/{org}/{repo}", ... } ``` -(source-files:repository)= -## Add a link to your repository +Three providers are supported: -To add a link to your repository, add the following configuration: +- **GitHub**: For example, `https://github.com/executablebooks/sphinx-book-theme`. + This includes custom URLs for self-hosted GitHub. +- **GitLab**: For example, `https://gitlab.com/gitlab-org/gitlab`. + This includes custom URLs for self-hosted GitLab. +- **BitBucket**: For example, `https://opensource.ncsa.illinois.edu/bitbucket/scm/u3d/3dutilities`. + +In each case, we **assume the final two URL items are the `org/repo` pair**. + +### Manually specify the provider + +If your provider URL is more complex (e.g., if you're self-hosting your provider), you can manually specify the provider with the following configuration: ```python html_theme_options = { ... - "repository_url": "https://github.com/{your-docs-url}", - "use_repository_button": True, + "repository_provider": "gitlab" # or "github", "bitbucket", + "repository_url": "selfhostedgh.mycompany.org/user/repo", ... } ``` -## Add a button to open issues +Once this is provided, you may add source buttons by following the following sections. -To add a button to open an issue about the current page, use the following -configuration: +(source-buttons:source)= +## Add a button to the page source + +Show the raw source of the page on the provider you've proivded. +To add a button to the page source, first [configure your source repository](source-buttons:repository) and then add: ```python html_theme_options = { ... - "repository_url": "https://github.com/{your-docs-url}", - "use_issues_button": True, + "use_source_button": True, + ... +} +``` + +Then configure the **repository branch** to use for your source. +By default it is `main`, but if you'd like to change this, use the following configuration: + +```python +html_theme_options = { + ... + "repository_branch": "{your-branch}", + ... +} +``` + +Finally, **configure the relative path to your documentation**. +By default, this is the root of the repository, but if your documentation is hosted in a sub-folder, use the following configuration: + +```python +html_theme_options = { + ... + "path_to_docs": "{path-relative-to-site-root}", ... } ``` ## Add a button to suggest edits -You can add a button to each page that will allow users to edit the page text -directly and submit a pull request to update the documentation. To include this -button, use the following configuration: +Allow users to edit the page text directly on the provider and submit a pull request to update the documentation. +To add a button to edit the page, first [configure your source repository](source-buttons:repository) and then add: ```python html_theme_options = { ... - "repository_url": "https://github.com/{your-docs-url}", "use_edit_page_button": True, ... } ``` -By default, the edit button will point to the `master` branch, but if you'd like -to change this, use the following configuration: +Then follow the [branch and relative path instructions in the source file section](source-buttons:source). + + +(source-files:repository)= +## Add a link to your repository + +To add a link to your repository, add the following configuration: ```python html_theme_options = { ... - "repository_branch": "{your-branch}", + "use_repository_button": True, ... } ``` -By default, the edit button will point to the root of the repository. If your -documentation is hosted in a sub-folder, use the following configuration: +## Add a button to open issues + +To add a button to open an issue about the current page, use the following +configuration: ```python html_theme_options = { ... - "path_to_docs": "{path-relative-to-site-root}", + "use_issues_button": True, ... } ``` diff --git a/docs/conf.py b/docs/conf.py index 19a5b75a..0ed47cad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -108,7 +108,7 @@ html_theme_options = { "path_to_docs": "docs", "repository_url": "https://github.com/executablebooks/sphinx-book-theme", - # "repository_branch": "gh-pages", # For testing + "repository_branch": "master", "launch_buttons": { "binderhub_url": "https://mybinder.org", "colab_url": "https://colab.research.google.com/", @@ -118,8 +118,9 @@ # "jupyterhub_url": "https://datahub.berkeley.edu", # For testing }, "use_edit_page_button": True, + "use_source_button": True, "use_issues_button": True, - "use_repository_button": True, + # "use_repository_button": True, "use_download_button": True, "use_sidenotes": True, "show_toc_level": 2, @@ -131,6 +132,25 @@ "image_dark": "_static/logo-wide-dark.svg", # "text": html_title, # Uncomment to try text with logo }, + "icon_links": [ + { + "name": "Executable Books", + "url": "https://executablebooks.org/", + "icon": "_static/ebp-logo.png", + "type": "local", + }, + { + "name": "GitHub", + "url": "https://github.com/executablebooks/sphinx-book-theme", + "icon": "fa-brands fa-github", + }, + { + "name": "PyPI", + "url": "https://pypi.org/project/sphinx-book-theme/", + "icon": "https://img.shields.io/pypi/dw/sphinx-book-theme", + "type": "url", + }, + ], # For testing # "use_fullscreen_button": False, # "home_page_in_toc": True, diff --git a/docs/content/launch.md b/docs/content/launch.md index d8dcc9ff..75fb7e17 100644 --- a/docs/content/launch.md +++ b/docs/content/launch.md @@ -62,7 +62,7 @@ To add Google Colab links to your page, add the following configuration: html_theme_options = { ... "launch_buttons": { - "colab_url": "https://{your-colab-url}" + "colab_url": "https://colab.research.google.com" }, ... } diff --git a/pyproject.toml b/pyproject.toml index 3718da21..ca31516e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ readme = "README.md" requires-python = ">=3.7" dependencies = [ "sphinx>=4,<7", - "pydata-sphinx-theme>=0.13.0rc4", + "pydata-sphinx-theme>=0.13.0rc5", ] license = { file = "LICENSE" } diff --git a/src/sphinx_book_theme/__init__.py b/src/sphinx_book_theme/__init__.py index 076d9f26..16e34676 100644 --- a/src/sphinx_book_theme/__init__.py +++ b/src/sphinx_book_theme/__init__.py @@ -9,10 +9,17 @@ from sphinx.application import Sphinx from sphinx.locale import get_translation from sphinx.util import logging +from pydata_sphinx_theme import _get_theme_options from .nodes import SideNoteNode -from .header_buttons import prep_header_buttons, add_header_buttons, update_sourcename +from .header_buttons import ( + prep_header_buttons, + add_header_buttons, + update_sourcename, + update_context_with_repository_info, +) from .header_buttons.launch import add_launch_buttons +from .header_buttons.source import add_source_buttons from ._transforms import HandleFootnoteTransform __version__ = "1.0.0rc2" @@ -60,7 +67,7 @@ def add_metadata_to_page(app, pagename, templatename, context, doctree): context["translate"] = translation # If search text hasn't been manually specified, use a shorter one here - theme_options = app.builder.theme_options or {} + theme_options = _get_theme_options(app) if "search_bar_text" not in theme_options: context["theme_search_bar_text"] = translation("Search") + "..." @@ -89,7 +96,7 @@ def hash_assets_for_files(assets: list, theme_static: Path, context): asset_sphinx_link = f"_static/{asset}" asset_source_path = theme_static / asset if not asset_source_path.exists(): - SPHINX_LOGGER.warn( + SPHINX_LOGGER.warning( f"Asset {asset_source_path} does not exist, not linking." ) # Find this asset in context, and update it to include the digest @@ -115,7 +122,7 @@ def hash_html_assets(app, pagename, templatename, context, doctree): def update_mode_thebe_config(app): """Update thebe configuration with SBT-specific values""" - theme_options = app.builder.theme_options + theme_options = _get_theme_options(app) if theme_options.get("launch_buttons", {}).get("thebe") is True: # In case somebody specifies they want thebe in a launch button # but has not activated the sphinx_thebe extension. @@ -154,7 +161,7 @@ def check_deprecation_keys(app): deprecated_config_list = ["single_page"] for key in deprecated_config_list: - if key in app.builder.theme_options: + if key in _get_theme_options(app): SPHINX_LOGGER.warning( f"'{key}' was deprecated from version 0.3.4 onwards. See the CHANGELOG for more information: https://github.com/executablebooks/sphinx-book-theme/blob/master/CHANGELOG.md" # noqa: E501 f"[{DEFAULT_LOG_TYPE}]", @@ -181,14 +188,14 @@ def run(self): return nodes -def update_general_config(app, config): +def update_general_config(app): theme_dir = get_html_theme_path() # Update templates for sidebar. Needed for jupyter-book builds as jb # uses an instance of Sphinx class from sphinx.application to build the app. # The __init__ function of which calls self.config.init_values() just # before emitting `config-inited` event. The init_values function overwrites # templates_path variable. - config.templates_path.append(os.path.join(theme_dir, "components")) + app.config.templates_path.append(os.path.join(theme_dir, "components")) def update_templates(app, pagename, templatename, context, doctree): @@ -227,7 +234,8 @@ def setup(app: Sphinx): app.connect("builder-inited", update_mode_thebe_config) app.connect("builder-inited", check_deprecation_keys) app.connect("builder-inited", update_sourcename) - app.connect("config-inited", update_general_config) + app.connect("builder-inited", update_context_with_repository_info) + app.connect("builder-inited", update_general_config) app.connect("html-page-context", add_metadata_to_page) app.connect("html-page-context", hash_html_assets) app.connect("html-page-context", update_templates) @@ -237,8 +245,9 @@ def setup(app: Sphinx): # Header buttons app.connect("html-page-context", prep_header_buttons) - app.connect("html-page-context", add_launch_buttons) - # Bump priority by 1 so that it runs after the pydata theme sets up the edit URL. + # Bump priority so that it runs after the pydata theme sets up the edit URL func. + app.connect("html-page-context", add_launch_buttons, priority=501) + app.connect("html-page-context", add_source_buttons, priority=501) app.connect("html-page-context", add_header_buttons, priority=501) # Directives diff --git a/src/sphinx_book_theme/_transforms.py b/src/sphinx_book_theme/_transforms.py index a990d9e5..77b4d8b8 100644 --- a/src/sphinx_book_theme/_transforms.py +++ b/src/sphinx_book_theme/_transforms.py @@ -2,6 +2,7 @@ from typing import Any from docutils import nodes as docutil_nodes from sphinx import addnodes as sphinx_nodes +from pydata_sphinx_theme import _get_theme_options from .nodes import SideNoteNode @@ -12,7 +13,7 @@ class HandleFootnoteTransform(SphinxPostTransform): formats = ("html",) def run(self, **kwargs: Any) -> None: - theme_options = self.app.builder.theme_options + theme_options = _get_theme_options(self.app) if theme_options.get("use_sidenotes", False) is False: return None # Cycle through footnote references, and move their content next to the diff --git a/src/sphinx_book_theme/assets/styles/components/_icon-links.scss b/src/sphinx_book_theme/assets/styles/components/_icon-links.scss new file mode 100644 index 00000000..bb6245b3 --- /dev/null +++ b/src/sphinx_book_theme/assets/styles/components/_icon-links.scss @@ -0,0 +1,21 @@ +/** + * Icon links design for the primary sidebar, where it defaults in this theme. + */ +.bd-sidebar-primary { + .navbar-icon-links { + column-gap: 0.5rem; + + .nav-link { + // There are few kinds of elements that can be icon links and each is different + i, + span { + font-size: 1.2rem; + } + + // Images usually fill more vertical space so we make them a bit smaller + img { + font-size: 0.8rem; + } + } + } +} diff --git a/src/sphinx_book_theme/assets/styles/index.scss b/src/sphinx_book_theme/assets/styles/index.scss index 1c42f7ed..099dadcd 100644 --- a/src/sphinx_book_theme/assets/styles/index.scss +++ b/src/sphinx_book_theme/assets/styles/index.scss @@ -27,6 +27,7 @@ @import "sections/footer-article"; // Re-usable components across the theme +@import "components/icon-links"; @import "components/logo"; @import "components/search"; diff --git a/src/sphinx_book_theme/header_buttons/__init__.py b/src/sphinx_book_theme/header_buttons/__init__.py index d1b7247e..ea081587 100644 --- a/src/sphinx_book_theme/header_buttons/__init__.py +++ b/src/sphinx_book_theme/header_buttons/__init__.py @@ -1,13 +1,17 @@ """Generate metadata for header buttons.""" - from sphinx.errors import SphinxError from sphinx.locale import get_translation +from pydata_sphinx_theme import _config_provided_by_user, _get_theme_options + +from sphinx.util import logging + +LOGGER = logging.getLogger(__name__) MESSAGE_CATALOG_NAME = "booktheme" translation = get_translation(MESSAGE_CATALOG_NAME) -def _as_bool(var): +def as_bool(var): """Cast string as a boolean with some extra checks. If var is a string, it will be matched to 'true'/'false' @@ -22,10 +26,21 @@ def _as_bool(var): return False -def _config_provided_by_user(app, key): - """Check if the user has manually provided the config.""" - # TODO: Remove this when we bump pydata to the latest version and import from there. - return any(key in ii for ii in [app.config.overrides, app.config._raw_config]) +def get_repo_parts(context): + """Return the parts of the source repository.""" + for provider in ["github", "bitbucket", "gitlab"]: + if f"{provider.lower()}_url" in context: + provider_url = context[f"{provider.lower()}_url"] + source_user = context[f"{provider.lower()}_user"] + source_repo = context[f"{provider.lower()}_repo"] + return provider_url, source_user, source_repo, provider + + +def get_repo_url(context): + """Return the provider URL based on what is defined in context.""" + provider_url, user, repo, provider = get_repo_parts(context) + repo_url = f"{provider_url}/{user}/{repo}" + return repo_url, provider def prep_header_buttons(app, pagename, templatename, context, doctree): @@ -34,112 +49,16 @@ def prep_header_buttons(app, pagename, templatename, context, doctree): def add_header_buttons(app, pagename, templatename, context, doctree): - """Populate the context with header button metadata we'll insert in templates.""" - opts = app.builder.theme_options + """Add basic and general header buttons, we'll add source/launch later.""" + opts = _get_theme_options(app) pathto = context["pathto"] header_buttons = context["header_buttons"] # If we have a suffix, then we have a source file suff = context.get("page_source_suffix") - # Full screen button - if _as_bool(opts.get("use_fullscreen_button", True)): - header_buttons.append( - { - "type": "javascript", - "javascript": "toggleFullScreen()", - "tooltip": translation("Fullscreen mode"), - "icon": "fas fa-expand", - } - ) - - # Edit this page button - # Add HTML context variables that the pydata theme uses that we configure elsewhere - # For some reason the source_suffix sometimes isn't there even when doctree is - repo_keywords = [ - "use_issues_button", - "use_edit_page_button", - "use_repository_button", - ] - for key in repo_keywords: - opts[key] = _as_bool(opts.get(key)) - - if any(opts.get(kw) for kw in repo_keywords): - repo_url = opts.get("repository_url", "") - if not repo_url: - raise SphinxError( - "Repository buttons enabled, but repository_url not given. " - "Please add a repository_url." - ) - repo_buttons = [] - if opts.get("use_repository_button"): - repo_buttons.append( - { - "type": "link", - "url": repo_url, - "tooltip": translation("Source repository"), - "text": "repository", - "icon": "fab fa-github", - } - ) - - if opts.get("use_issues_button"): - repo_buttons.append( - { - "type": "link", - "url": f"{repo_url}/issues/new?title=Issue%20on%20page%20%2F{context['pagename']}.html&body=Your%20issue%20content%20here.", # noqa: E501 - "text": translation("open issue"), - "tooltip": translation("Open an issue"), - "icon": "fas fa-lightbulb", - } - ) - - if opts.get("use_edit_page_button") and doctree and suff: - branch = opts.get("repository_branch", "") - if branch == "": - branch = "master" - relpath = opts.get("path_to_docs", "") - org, repo = repo_url.strip("/").split("/")[-2:] - - # Update the context because this is what the get_edit_url function uses. - context.update( - { - "github_user": org, - "github_repo": repo, - "github_version": branch, - "doc_path": relpath, - } - ) - - provider, url = context["get_edit_provider_and_url"]() - repo_buttons.append( - { - "type": "link", - "url": url, - "tooltip": translation("Edit on") + f"{provider}", - "text": translation("suggest edit"), - "icon": "fas fa-pencil-alt", - } - ) - - # If we have multiple repo buttons enabled, add a group, otherwise just 1 button - if len(repo_buttons) > 1: - header_buttons.append( - { - "type": "group", - "tooltip": translation("Source repositories"), - "icon": "fab fa-github", - "buttons": repo_buttons, - "label": "repository-buttons", - } - ) - elif len(repo_buttons) == 1: - # Remove the text since it's just a single button, want just an icon. - repo_buttons[0]["text"] = "" - header_buttons.extend(repo_buttons) - # Download buttons for various source content. - if _as_bool(opts.get("use_download_button", True)) and suff: + if as_bool(opts.get("use_download_button", True)) and suff: download_buttons = [] # An ipynb file if it was created as part of the build (e.g. by MyST-NB) @@ -151,6 +70,7 @@ def add_header_buttons(app, pagename, templatename, context, doctree): "text": ".ipynb", "icon": "fas fa-code", "tooltip": translation("Download notebook file"), + "label": "download-notebook-button", } ) @@ -162,6 +82,7 @@ def add_header_buttons(app, pagename, templatename, context, doctree): "text": suff, "tooltip": translation("Download source file"), "icon": "fas fa-file", + "label": "download-source-button", } ) download_buttons.append( @@ -171,6 +92,7 @@ def add_header_buttons(app, pagename, templatename, context, doctree): "text": ".pdf", "tooltip": translation("Print to PDF"), "icon": "fas fa-file-pdf", + "label": "download-pdf-button", } ) @@ -185,6 +107,18 @@ def add_header_buttons(app, pagename, templatename, context, doctree): } ) + # Full screen button + if as_bool(opts.get("use_fullscreen_button", True)): + header_buttons.append( + { + "type": "javascript", + "javascript": "toggleFullScreen()", + "tooltip": translation("Fullscreen mode"), + "icon": "fas fa-expand", + "label": "fullscreen-button", + } + ) + def update_sourcename(app): # Download the source file @@ -195,3 +129,62 @@ def update_sourcename(app): # If a key isn't in it, then the user didn't provide it if not _config_provided_by_user(app, "html_sourcelink_suffix"): app.config.html_sourcelink_suffix = "" + + +def update_context_with_repository_info(app): + """Update pydata `html_context` options for source from `repository_url`. + + We do this because we use repository_url as one config to define the URL, + while the PST uses a collection of {provider}_{key} pairs in html_context. + So here we insert those context variables on our own. + """ + opts = _get_theme_options(app) + context = app.config.html_context + + # This is the way to give repository info. If it doesn't exist, do nothing. + repo_url = opts.get("repository_url", "") + if not repo_url: + return + + # Check for manually given options first + branch = opts.get("repository_branch", "") + provider = opts.get("repository_provider", "") + relpath = opts.get("path_to_docs", "") + if branch == "": + branch = "main" + + # We assume the final two parts of the repository URL are the org/repo + provider_url, org, repo = repo_url.strip("/").rsplit("/", 2) + + # Infer the provider if it wasn't manually given + default_provider_urls = { + "bitbucket": "bitbucket.org", + "github": "github.com", + "gitlab": "gitlab.com", + } + + # If no provider is given, try to infer one from the repo url + if provider == "": + for iprov in default_provider_urls.keys(): + if iprov in provider_url.lower(): + provider = iprov + break + + # If provider is still empty, raise an error because we don't recognize it + if provider == "": + raise SphinxError( + ( + f"Provider not recognized in repository url {repo_url}. " + "If you're using a custom provider URL, specify `repository_provider`" + ) + ) + + # Update the context because this is what the get_edit_url function uses. + repository_information = { + f"{provider}_user": org, + f"{provider}_repo": repo, + f"{provider}_version": branch, + f"{provider}_url": provider_url, + "doc_path": relpath, + } + context.update(repository_information) diff --git a/src/sphinx_book_theme/header_buttons/launch.py b/src/sphinx_book_theme/header_buttons/launch.py index 77da4dbd..112872d8 100644 --- a/src/sphinx_book_theme/header_buttons/launch.py +++ b/src/sphinx_book_theme/header_buttons/launch.py @@ -1,6 +1,7 @@ +"""Launch buttons for Binder / Thebe / Colab / etc.""" from pathlib import Path from typing import Any, Dict, Optional -from urllib.parse import urlencode +from urllib.parse import urlencode, quote from docutils.nodes import document from sphinx.application import Sphinx @@ -8,6 +9,8 @@ from sphinx.util import logging from shutil import copy2 +from . import get_repo_parts, get_repo_url + SPHINX_LOGGER = logging.getLogger(__name__) @@ -34,18 +37,17 @@ def add_launch_buttons( """ - # 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 + # Don't do anything if no launch provider is configured config_theme = app.config["html_theme_options"] launch_buttons = config_theme.get("launch_buttons", {}) if ( not launch_buttons - or not _is_notebook(app, pagename) + or not _is_notebook(app, context) or not any( - launch_buttons[key] + launch_buttons.get(key) for key in ("binderhub_url", "jupyterhub_url", "thebe", "colab_url") ) ): @@ -55,7 +57,7 @@ def add_launch_buttons( 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 ( + if _is_notebook(app, context) and ( context["sourcename"].endswith(".md") or context["sourcename"].endswith(".md.txt") ): @@ -72,10 +74,9 @@ def add_launch_buttons( 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) + # Get repository URL information that we'll use to build links + repo_url, _ = get_repo_url(context) + provider_url, org, repo, provider = get_repo_parts(context) if org is None and repo is None: # Skip the rest because the repo_url isn't right return @@ -114,11 +115,20 @@ def add_launch_buttons( binderhub_url = launch_buttons.get("binderhub_url", "").strip("/") colab_url = launch_buttons.get("colab_url", "").strip("/") deepnote_url = launch_buttons.get("deepnote_url", "").strip("/") + + # Loop through each provider and add a button for it if needed if binderhub_url: - url = ( - f"{binderhub_url}/v2/gh/{org}/{repo}/{branch}?" - f"urlpath={ui_pre}/{path_rel_repo}" - ) + # Any non-standard repository URL should be passed-through raw + if provider_url not in ["https://github.com", "https://gitlab.com"]: + # Generic git repository using the full repo URL as a fallback + url = f"{binderhub_url}/v2/git/{quote(repo_url)}/{branch}" + elif provider.lower() == "github": + url = f"{binderhub_url}/v2/gh/{org}/{repo}/{branch}" + elif provider.lower() == "gitlab": + # Binder uses %2F for gitlab for some reason + url = f"{binderhub_url}/v2/gl/{org}%2F{repo}/{branch}" + + url = f"{url}?urlpath={ui_pre}/{path_rel_repo}" launch_buttons_list.append( { "type": "link", @@ -148,29 +158,35 @@ def add_launch_buttons( ) if colab_url: - url = f"{colab_url}/github/{org}/{repo}/blob/{branch}/{path_rel_repo}" - launch_buttons_list.append( - { - "type": "link", - "text": "Colab", - "tooltip": translation("Launch on") + "Colab", - "icon": "_static/images/logo_colab.png", - "url": url, - } - ) + if provider.lower() != "github": + SPHINX_LOGGER.warning(f"Provider {provider} not supported on colab.") + else: + url = f"{colab_url}/github/{org}/{repo}/blob/{branch}/{path_rel_repo}" + launch_buttons_list.append( + { + "type": "link", + "text": "Colab", + "tooltip": translation("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": translation("Launch on") + "Deepnote", - "icon": "_static/images/logo_deepnote.svg", - "url": url, - } - ) + if provider.lower() != "github": + SPHINX_LOGGER.warning(f"Provider {provider} not supported on Deepnote.") + else: + 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": translation("Launch on") + "Deepnote", + "icon": "_static/images/logo_deepnote.svg", + "url": url, + } + ) # Add thebe flag in context if launch_buttons.get("thebe", False): @@ -211,17 +227,17 @@ def _split_repo_url(url): 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 _is_notebook(app, context): + pagename = context["pagename"] + metadata = app.env.metadata[pagename] + if "kernelspec" in metadata: + # Most notebooks will have this + return True + elif "ipynb" in context.get("page_source_suffix", ""): + # Just in case, check for the suffix since some people remove the kernelspec + return True + else: + return False def _get_branch(config_theme): diff --git a/src/sphinx_book_theme/header_buttons/source.py b/src/sphinx_book_theme/header_buttons/source.py new file mode 100644 index 00000000..8ea3b229 --- /dev/null +++ b/src/sphinx_book_theme/header_buttons/source.py @@ -0,0 +1,117 @@ +"""Source file buttons that point to the online repository.""" + +from pydata_sphinx_theme import _get_theme_options +from sphinx.locale import get_translation +from sphinx.util import logging + +from . import as_bool, get_repo_url + +LOGGER = logging.getLogger(__name__) +MESSAGE_CATALOG_NAME = "booktheme" +translation = get_translation(MESSAGE_CATALOG_NAME) + + +def add_source_buttons(app, pagename, templatename, context, doctree): + """Add the source repository buttons.""" + opts = _get_theme_options(app) + header_buttons = context["header_buttons"] + # If we have a suffix, then we have a source file + suff = context.get("page_source_suffix") + + # Add HTML context variables that the pydata theme uses that we configure elsewhere + # For some reason the source_suffix sometimes isn't there even when doctree is + repo_keywords = [ + "use_issues_button", + "use_source_button", + "use_edit_page_button", + "use_repository_button", + ] + for key in repo_keywords: + opts[key] = as_bool(opts.get(key)) + + # Create source buttons for any that are enabled + if any(opts.get(kw) for kw in repo_keywords): + # Loop through the possible buttons and construct+add their URL + repo_buttons = [] + if opts.get("use_repository_button"): + repo_url, provider = get_repo_url(context) + + repo_buttons.append( + { + "type": "link", + "url": repo_url, + "tooltip": translation("Source repository"), + "text": "Repository", + "icon": f"fab fa-{provider.lower()}", + "label": "source-repository-button", + } + ) + + if opts.get("use_source_button") and doctree and suff: + # We'll re-use this to make action-specific URLs + provider, edit_url = context["get_edit_provider_and_url"]() + # Convert URL to a blob so it's for viewing + if provider.lower() == "github": + # Use plain=1 to ensure the source text is shown, not rendered + source_url = edit_url.replace("/edit/", "/blob/") + "?plain=1" + elif provider.lower() == "gitlab": + source_url = edit_url.replace("/edit/", "/blob/") + elif provider.lower() == "bitbucket": + source_url = edit_url.replace("?mode=edit", "") + + repo_buttons.append( + { + "type": "link", + "url": source_url, + "tooltip": translation("Show source"), + "text": translation("Show source"), + "icon": "fas fa-code", + "label": "source-file-button", + } + ) + + if opts.get("use_edit_page_button") and doctree and suff: + # We'll re-use this to make action-specific URLs + provider, edit_url = context["get_edit_provider_and_url"]() + repo_buttons.append( + { + "type": "link", + "url": edit_url, + "tooltip": translation("Suggest edit"), + "text": translation("Suggest edit"), + "icon": "fas fa-pencil-alt", + "label": "source-edit-button", + } + ) + + if opts.get("use_issues_button"): + repo_url, provider = get_repo_url(context) + if provider != "github": + LOGGER.warning(f"Open issue button not yet supported for {provider}") + else: + repo_buttons.append( + { + "type": "link", + "url": f"{repo_url}/issues/new?title=Issue%20on%20page%20%2F{context['pagename']}.html&body=Your%20issue%20content%20here.", # noqa: E501 + "text": translation("Open issue"), + "tooltip": translation("Open an issue"), + "icon": "fas fa-lightbulb", + "label": "source-issues-button", + } + ) + + # If we have multiple repo buttons enabled, add a group, otherwise just 1 button + if len(repo_buttons) > 1: + header_buttons.append( + { + "type": "group", + "tooltip": translation("Source repositories"), + "icon": f"fab fa-{provider.lower()}", + "buttons": repo_buttons, + "label": "source-buttons", + } + ) + elif len(repo_buttons) == 1: + # Remove the text since it's just a single button, want just an icon. + repo_buttons[0]["text"] = "" + header_buttons.extend(repo_buttons) 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 index 5cbc2b5d..c61868d0 100644 --- a/src/sphinx_book_theme/theme/sphinx_book_theme/macros/buttons.html +++ b/src/sphinx_book_theme/theme/sphinx_book_theme/macros/buttons.html @@ -36,10 +36,10 @@ {% endmacro %} -{% macro render_button_group(buttons, icon, tooltip=None, label=None) %} +{% macro render_button_group(buttons, icon, tooltip=None, label=None, classes="") %} {# A bootstrap dropdown #} {# Bootstrap dropdown ref: https://getbootstrap.com/docs/5.2/components/dropdowns/ #} -