diff --git a/docs/_templates/edit-this-page.html b/docs/_templates/edit-this-page.html index 0254d1a2a2..c474a4a7e3 100644 --- a/docs/_templates/edit-this-page.html +++ b/docs/_templates/edit-this-page.html @@ -1,9 +1,9 @@ {% if sourcename is defined and theme_use_edit_page_button and page_source_suffix %} {% set src = sourcename.split('.') %}
- + {% set provider, url = get_edit_provider_and_url() %} + - {% set provider = get_edit_provider_and_url()[0] %} {% block edit_this_page_text %} {% if provider %} {% trans provider=provider %}Edit on {{ provider }}{% endtrans %} diff --git a/docs/user_guide/header-links.rst b/docs/user_guide/header-links.rst index bf41bbccd7..2b74b1d34c 100644 --- a/docs/user_guide/header-links.rst +++ b/docs/user_guide/header-links.rst @@ -287,6 +287,8 @@ These may be removed in a future release in favor of ``icon_links``: "github_url": "https://github.com//", "gitlab_url": "https://gitlab.com//", "bitbucket_url": "https://bitbucket.org//", + "forgejo_url": "https://codeberg.org//", + "gitea_url": "https://gitea.com//", "twitter_url": "https://twitter.com/", ... } diff --git a/docs/user_guide/source-buttons.rst b/docs/user_guide/source-buttons.rst index a4db469adf..b492904708 100644 --- a/docs/user_guide/source-buttons.rst +++ b/docs/user_guide/source-buttons.rst @@ -19,7 +19,7 @@ your ``conf.py`` file in 'html_theme_options': } A number of providers are available for building *Edit this Page* links, including -GitHub, GitLab, and Bitbucket. For each, the default public instance URL can be +GitHub, GitLab, Bitbucket, Forgejo and Gitea. For each, the default public instance URL (should it exist) can be replaced with a self-hosted instance. @@ -65,6 +65,33 @@ Bitbucket } +Forgejo +--------- + +.. code:: python + + html_context = { + # "forgejo_url": "https://codeberg.org", # or your self-hosted Forgejo + "forgejo_user": "", + "forgejo_repo": "", + "forgejo_version": "", + "doc_path": "", + } + + +Gitea +--------- + +.. code:: python + + html_context = { + # "gitea_url": "https://gitea.com", # or your self-hosted Gitea + "gitea_user": "", + "gitea_repo": "", + "gitea_version": "", + "doc_path": "", + } + Custom Edit URL --------------- @@ -80,7 +107,7 @@ any other context values. "some_other_arg": "?some-other-arg" } -With the predefined providers, the link text reads "Edit on GitHub/GitLab/Bitbucket". +With the predefined providers, the link text reads "Edit on GitHub/GitLab/Bitbucket/Codeberg/Forgejo/Gitea". By default, a simple "Edit" is used if you use a custom URL. However, you can set a provider name like this: diff --git a/docs/user_guide/theme-elements.md b/docs/user_guide/theme-elements.md index 3a40bec5be..4fb892f77f 100644 --- a/docs/user_guide/theme-elements.md +++ b/docs/user_guide/theme-elements.md @@ -238,6 +238,37 @@ For example: There are a variety of link targets supported, here's a table for reference: +**Codeberg** + +- `https://codeberg.org`: https://codeberg.org +- `https://codeberg.org/c-org`: https://codeberg.org/c-org +- `https://codeberg.org/c-org/c-repo`: https://codeberg.org/c-org/c-repo +- `https://codeberg.org/c-org/c-repo/issues/375583`: https://codeberg.org/c-org/c-repo/issues/375583 +- `https://codeberg.org/c-org/c-repo/pulls/1012`: https://codeberg.org/c-org/c-repo/pulls/1012 + +**Forgejo** + +Since self-hosted Forgejo instances can't be easily identified, only links specified +in `forgejo_url` (links to project's own forge) or domains containing "forgejo" +leading to issues, pull requests or projects are shortened. + +- `https://my-forgejo.org`: https://my-forgejo.org +- `https://forgejo.my.org/forgejo-org/forgejo`: https://my-forgejo.org/forgejo-org/forgejo/pulls +- `https://my-forgejo.com/forgejo-org/forgejo/issues/375583`: https://my-forgejo.org/forgejo-org/forgejo/issues/375583 +- `https://my.forgejo.org/forgejo-org/forgejo/pulls/1012`: https://my.forgejo.org/forgejo-org/forgejo/pulls/1012 + +**Gitea** + +`https://gitea.com` is always identified, while the same rules in place for Forgejo +apply to self-hosted instances. + +- `https://gitea.com`: https://gitea.com +- `https://my-gitea.com`: https://my-gitea.com +- `https://gitea.com/gitea-org`: https://gitea.com/gitea-org +- `https://gitea.com/gitea-org/gitea`: https://gitea.com/gitea-org/gitea +- `https://my-gitea.com/gitea-org/gitea/issues/375583`: https://my-gitea.com/gitea-org/gitea/issues/375583 +- `https://gitea.my.com/gitea-org/gitea/pulls/1012`: https://gitea.my.com/gitea-org/gitea/pulls/1012 + **GitHub** - `https://github.com`: https://github.com diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 0b1ed7d4fe..c4837f73a0 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -132,15 +132,27 @@ def update_config(app): # Handle icon link shortcuts shortcuts = [ - ("twitter_url", "fa-brands fa-square-twitter", "Twitter"), - ("bitbucket_url", "fa-brands fa-bitbucket", "Bitbucket"), - ("gitlab_url", "fa-brands fa-square-gitlab", "GitLab"), - ("github_url", "fa-brands fa-square-github", "GitHub"), + ("twitter_url", "fa-brands fa-square-twitter", "Twitter", "fontawesome"), + ("bitbucket_url", "fa-brands fa-bitbucket", "Bitbucket", "fontawesome"), + ("gitlab_url", "fa-brands fa-square-gitlab", "GitLab", "fontawesome"), + ("github_url", "fa-brands fa-square-github", "GitHub", "fontawesome"), + ( + "forgejo_url", + r"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 212 212'%3E%3Cstyle%3Ecircle,path%7Bfill:none;stroke:%23000;stroke-width:15%7Dpath%7Bstroke-width:25%7D.orange%7Bstroke:%23f60%7D.red%7Bstroke:%23d40000%7D%3C/style%3E%3Cg transform='translate(6 6)'%3E%3Cpath d='M58 168V70a50 50 0 0 1 50-50h20' class='orange'/%3E%3Cpath d='M58 168v-30a50 50 0 0 1 50-50h20' class='red'/%3E%3Ccircle cx='142' cy='20' r='18' class='orange'/%3E%3Ccircle cx='142' cy='88' r='18' class='red'/%3E%3Ccircle cx='58' cy='180' r='18' class='red'/%3E%3C/g%3E%3C/svg%3E", + "Forgejo", + "local", + ), + ( + "gitea_url", + r"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath fill='%23fff' d='m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z'/%3E%3Cg fill='%23609926'%3E%3Cpath d='M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z'/%3E%3Cpath d='M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z'/%3E%3C/g%3E%3C/svg%3E", + "Gitea", + "local", + ), ] # Add extra icon links entries if there were shortcuts present # TODO: Deprecate this at some point in the future? icon_links = theme_options.get("icon_links", []) - for url, icon, name in shortcuts: + for url, icon, name, icon_type in shortcuts: if theme_options.get(url): # This defaults to an empty list so we can always insert icon_links.insert( @@ -149,9 +161,10 @@ def update_config(app): "url": theme_options.get(url), "icon": icon, "name": name, - "type": "fontawesome", + "type": icon_type, }, ) + icon_links[0] = adjust_known_instances(icon_links[0]) theme_options["icon_links"] = icon_links # Prepare the logo config dictionary @@ -275,6 +288,16 @@ def _fix_canonical_url( context["pageurl"] = app.config.html_baseurl + target +def adjust_known_instances(icon_link: dict[str, str]) -> dict[str, str]: + """Adjust icon data for supported self-hostable forge instances.""" + if icon_link["url"].startswith("https://codeberg.org"): + icon_link["icon"] = ( + r"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 4.233 4.233'%3E%3Cdefs%3E%3ClinearGradient xlink:href='%23a' id='b' x1='42519.285' x2='42575.336' y1='-7078.789' y2='-6966.931' gradientUnits='userSpaceOnUse'/%3E%3ClinearGradient id='a'%3E%3Cstop offset='0' style='stop-color:%232185d0;stop-opacity:0'/%3E%3Cstop offset='.495' style='stop-color:%232185d0;stop-opacity:.30000001'/%3E%3Cstop offset='1' style='stop-color:%232185d0;stop-opacity:.30000001'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M42519.285-7078.79a.76.568 0 0 0-.738.675l33.586 125.888a87.182 87.182 0 0 0 39.381-33.763l-71.565-92.52a.76.568 0 0 0-.664-.28z' style='font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(%23b);fill-opacity:1;stroke:none;stroke-width:3.67846;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill;stop-color:%23000;stop-opacity:1' transform='translate(-1030.156 172.97) scale(.02428)'/%3E%3Cpath d='M11249.461-1883.696c-12.74 0-23.067 10.327-23.067 23.067 0 4.333 1.22 8.58 3.522 12.251l19.232-24.863c.138-.18.486-.18.624 0l19.233 24.864a23.068 23.068 0 0 0 3.523-12.252c0-12.74-10.327-23.067-23.067-23.067z' style='opacity:1;fill:%232185d0;fill-opacity:1;stroke-width:17.0055;paint-order:markers fill stroke;stop-color:%23000' transform='translate(-1030.156 172.97) scale(.09176)'/%3E%3C/svg%3E" + ) + icon_link["name"] = "Codeberg" + return icon_link + + def setup(app: Sphinx) -> Dict[str, str]: """Setup the Sphinx application.""" here = Path(__file__).parent.resolve() diff --git a/src/pydata_sphinx_theme/assets/styles/base/_base.scss b/src/pydata_sphinx_theme/assets/styles/base/_base.scss index 82235df440..c050f47c04 100644 --- a/src/pydata_sphinx_theme/assets/styles/base/_base.scss +++ b/src/pydata_sphinx_theme/assets/styles/base/_base.scss @@ -46,16 +46,48 @@ a { user-select: none; } - // set up a icon next to the shorten links from github and gitlab + // set up an icon next to the shortened links from forges + &.codeberg, + &.forgejo, + &.gitea, &.github, &.gitlab { &::before { color: var(--pst-color-text-muted); - font: var(--fa-font-brands); margin-right: 0.25rem; } } + &.codeberg, + &.forgejo, + &.gitea { + &::before { + display: inline-block; + vertical-align: -15%; + width: calc(var(--pst-font-size-icon) - 0.5rem); + height: calc(var(--pst-font-size-icon) - 0.5rem); + } + } + + &.github, + &.gitlab { + &::before { + font: var(--fa-font-brands); + } + } + + &.codeberg::before { + content: var(--pst-icon-codeberg); + } + + &.forgejo::before { + content: var(--pst-icon-forgejo); + } + + &.gitea::before { + content: var(--pst-icon-gitea); + } + &.github::before { content: var(--pst-icon-github); } diff --git a/src/pydata_sphinx_theme/assets/styles/variables/_icons.scss b/src/pydata_sphinx_theme/assets/styles/variables/_icons.scss index f7618ec4ed..b32b49b953 100644 --- a/src/pydata_sphinx_theme/assets/styles/variables/_icons.scss +++ b/src/pydata_sphinx_theme/assets/styles/variables/_icons.scss @@ -24,6 +24,17 @@ html { --pst-icon-bell: "\f0f3"; // fa-solid fa-bell --pst-icon-pencil: "\f303"; // fa-solid fa-pencil + // SVG icons + // minified & encoded by SVGOMG 3.0.0 + https://yoksel.github.io/url-encoder/ + // https://codeberg.org/Codeberg/Design/src/commit/ac514aa9aaa2457d4af3c3e13df3ab136d22a49a/logo/icon/svg/codeberg-logo_icon_blue.svg + --pst-icon-codeberg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 4.233 4.233'%3E%3Cdefs%3E%3ClinearGradient xlink:href='%23a' id='b' x1='42519.285' x2='42575.336' y1='-7078.789' y2='-6966.931' gradientUnits='userSpaceOnUse'/%3E%3ClinearGradient id='a'%3E%3Cstop offset='0' style='stop-color:%232185d0;stop-opacity:0'/%3E%3Cstop offset='.495' style='stop-color:%232185d0;stop-opacity:.30000001'/%3E%3Cstop offset='1' style='stop-color:%232185d0;stop-opacity:.30000001'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M42519.285-7078.79a.76.568 0 0 0-.738.675l33.586 125.888a87.182 87.182 0 0 0 39.381-33.763l-71.565-92.52a.76.568 0 0 0-.664-.28z' style='font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(%23b);fill-opacity:1;stroke:none;stroke-width:3.67846;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill;stop-color:%23000;stop-opacity:1' transform='translate(-1030.156 172.97) scale(.02428)'/%3E%3Cpath d='M11249.461-1883.696c-12.74 0-23.067 10.327-23.067 23.067 0 4.333 1.22 8.58 3.522 12.251l19.232-24.863c.138-.18.486-.18.624 0l19.233 24.864a23.068 23.068 0 0 0 3.523-12.252c0-12.74-10.327-23.067-23.067-23.067z' style='opacity:1;fill:%232185d0;fill-opacity:1;stroke-width:17.0055;paint-order:markers fill stroke;stop-color:%23000' transform='translate(-1030.156 172.97) scale(.09176)'/%3E%3C/svg%3E"); + + // https://codeberg.org/forgejo/forgejo/src/commit/0ce1f708202c966ea190dc1f91b0d527ceb09c11/assets/logo.svg + --pst-icon-forgejo: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 212 212'%3E%3Cstyle%3Ecircle,path%7Bfill:none;stroke:%23000;stroke-width:15%7Dpath%7Bstroke-width:25%7D.orange%7Bstroke:%23f60%7D.red%7Bstroke:%23d40000%7D%3C/style%3E%3Cg transform='translate(6 6)'%3E%3Cpath d='M58 168V70a50 50 0 0 1 50-50h20' class='orange'/%3E%3Cpath d='M58 168v-30a50 50 0 0 1 50-50h20' class='red'/%3E%3Ccircle cx='142' cy='20' r='18' class='orange'/%3E%3Ccircle cx='142' cy='88' r='18' class='red'/%3E%3Ccircle cx='58' cy='180' r='18' class='red'/%3E%3C/g%3E%3C/svg%3E"); + + // https://gitea.com/gitea/design/src/commit/4f571b3c617d2330dc066c5f076f62ac84723dd2/logo/logo.svg + --pst-icon-gitea: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath fill='%23fff' d='m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z'/%3E%3Cg fill='%23609926'%3E%3Cpath d='M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z'/%3E%3Cpath d='M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z'/%3E%3C/g%3E%3C/svg%3E"); + // Bootstrap icons --pst-breadcrumb-divider: "\f105"; } diff --git a/src/pydata_sphinx_theme/edit_this_page.py b/src/pydata_sphinx_theme/edit_this_page.py index c54fe20f68..5395675170 100644 --- a/src/pydata_sphinx_theme/edit_this_page.py +++ b/src/pydata_sphinx_theme/edit_this_page.py @@ -1,10 +1,14 @@ """Create an "edit this page" url compatible with bitbucket, gitlab and github.""" +import urllib + import jinja2 from sphinx.application import Sphinx from sphinx.errors import ExtensionError +from .utils import get_theme_options_dict + def setup_edit_url( app: Sphinx, pagename: str, templatename: str, context, doctree @@ -20,11 +24,24 @@ def get_edit_provider_and_url() -> None: if doc_path and not doc_path.endswith("/"): doc_path = f"{doc_path}/" - default_provider_urls = { + provider_urls = { "bitbucket_url": "https://bitbucket.org", + "forgejo_url": "https://codeberg.org", + "gitea_url": "https://gitea.com", "github_url": "https://github.com", "gitlab_url": "https://gitlab.com", } + provider_labels = { + "bitbucket": "Bitbucket", + "forgejo": "Forgejo", + "gitea": "Gitea", + "github": "GitHub", + "gitlab": "GitLab", + } + theme_options = get_theme_options_dict(app) + provider_urls, provider_labels = adjust_forge_params( + provider_urls, provider_labels, theme_options + ) edit_attrs = {} @@ -44,25 +61,35 @@ def get_edit_provider_and_url() -> None: edit_attrs.update( { ("bitbucket_user", "bitbucket_repo", "bitbucket_version"): ( - "Bitbucket", + provider_labels["bitbucket"], "{{ bitbucket_url }}/{{ bitbucket_user }}/{{ bitbucket_repo }}" "/src/{{ bitbucket_version }}" "/{{ doc_path }}{{ file_name }}?mode=edit", ), + ("forgejo_user", "forgejo_repo", "forgejo_version"): ( + provider_labels["forgejo"], + "{{ forgejo_url }}/{{ forgejo_user }}/{{ forgejo_repo }}" + "/_edit/{{ forgejo_version }}/{{ doc_path }}{{ file_name }}", + ), + ("gitea_user", "gitea_repo", "gitea_version"): ( + provider_labels["gitea"], + "{{ gitea_url }}/{{ gitea_user }}/{{ gitea_repo }}" + "/_edit/{{ gitea_version }}/{{ doc_path }}{{ file_name }}", + ), ("github_user", "github_repo", "github_version"): ( - "GitHub", + provider_labels["github"], "{{ github_url }}/{{ github_user }}/{{ github_repo }}" "/edit/{{ github_version }}/{{ doc_path }}{{ file_name }}", ), ("gitlab_user", "gitlab_repo", "gitlab_version"): ( - "GitLab", + provider_labels["gitlab"], "{{ gitlab_url }}/{{ gitlab_user }}/{{ gitlab_repo }}" "/-/edit/{{ gitlab_version }}/{{ doc_path }}{{ file_name }}", ), } ) - doc_context = dict(default_provider_urls) + doc_context = dict(provider_urls) doc_context.update(context) doc_context.update(doc_path=doc_path, file_name=file_name) @@ -80,3 +107,27 @@ def get_edit_provider_and_url() -> None: # Ensure that the max TOC level is an integer context["theme_show_toc_level"] = int(context.get("theme_show_toc_level", 1)) + + +def adjust_forge_params( + forge_urls: dict[str, str], + forge_labels: dict[str, str], + theme_options: dict[str, str], +) -> (dict[str, str], dict[str, str]): + """Adjust labels and URLs for some of the more decentralized forges.""" + # use *_urls given in html_theme_options as authority (netloc) + # instead of default if available + for url_key in forge_urls: + forge_url = theme_options.get(url_key) + if forge_url: + forge_url = urllib.parse.urlsplit(forge_url, allow_fragments=False) + forge_url = f"{forge_url.scheme}://{forge_url.netloc}" + forge_urls[url_key] = forge_url + + # use rebranded forge label instead of generic SW name where known + if "forgejo_url" in theme_options: + url = theme_options["forgejo_url"] + if url.startswith("https://codeberg.org"): + forge_labels["forgejo"] = "Codeberg" + + return forge_urls, forge_labels diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index 34db161e49..3ccc936f63 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -1,19 +1,20 @@ """A custom Transform object to shorten github and gitlab links.""" -from typing import ClassVar +from typing import ClassVar, Literal from urllib.parse import ParseResult, urlparse, urlunparse from docutils import nodes from sphinx.transforms.post_transforms import SphinxPostTransform from sphinx.util.nodes import NodeMatcher -from .utils import traverse_or_findall +from .utils import get_theme_options_dict, traverse_or_findall class ShortenLinkTransform(SphinxPostTransform): """ - Shorten link when they are coming from github or gitlab and add an extra class to - the tag for further styling. + Shorten links leading to supported forges. + Also attempt to identify self-hosted forge instances. + Add an extra class to the tag for further styling. Before: .. code-block:: html @@ -35,6 +36,8 @@ class ShortenLinkTransform(SphinxPostTransform): default_priority = 400 formats = ("html",) supported_platform: ClassVar[dict[str, str]] = { + "codeberg.org": "codeberg", + "gitea.com": "gitea", "github.com": "github", "gitlab.com": "gitlab", } @@ -53,10 +56,54 @@ def run(self, **kwargs): uri = urlparse(uri) # only do something if the platform is identified self.platform = self.supported_platform.get(uri.netloc) + # or we can make a reasonable guess about self-hosted forges + if self.platform is None: + html_theme_options = get_theme_options_dict(self.app) + self.platform = self.identify_selfhosted(uri, html_theme_options) if self.platform is not None: node.attributes["classes"].append(self.platform) node.children[0] = nodes.Text(self.parse_url(uri)) + def identify_selfhosted( + self, uri: ParseResult, html_theme_options: dict[str, str] + ) -> Literal["forgejo", "gitea", "gitlab"] | None: + """Try to identify what self-hosted forge uri leads to (if any). + + Args: + uri: the link to the platform content + html_theme_options: varia + + Returns: + likely platform if one matches, None otherwise + """ + # forge name in authority and known url part in the right place + # unreliable but may catch any number of hosts + path_parts = uri.path.strip("/").split("/") + if len(path_parts) > 2 and path_parts[2] in ("pulls", "issues", "projects"): + if "forgejo" in uri.netloc: + return "forgejo" + elif "gitea" in uri.netloc: + return "gitea" + if ( + len(path_parts) > 3 + and path_parts[2] == "-" + and path_parts[3] in ("issues", "merge_requests") + ): + if "gitlab" in uri.netloc: + return "gitlab" + + # url passed in *_url option + # will only match project's own forge but that's + # likely where most doc links will lead anyway + str_url = f"{uri.scheme}://{uri.netloc}" + selfhosted = ("forgejo", "gitea", "gitlab") + for forge in selfhosted: + known_url = html_theme_options.get(f"{forge}_url") + if known_url and known_url.startswith(str_url): + return forge + + return None + def parse_url(self, uri: ParseResult) -> str: """Parse the content of the url with respect to the selected platform. @@ -119,5 +166,14 @@ def parse_url(self, uri: ParseResult) -> str: # for example "///" text = uri._replace(netloc="", scheme="") # remove platform text = urlunparse(text)[1:] # combine to string and strip leading "/" + elif self.platform in ("codeberg", "forgejo", "gitea"): + parts = path.rstrip("/").split("/") + if len(parts) == 4 and parts[2] in ("issues", "pulls"): + text = f"{parts[0]}/{parts[1]}#{parts[3]}" # element number + elif parts == [""]: + text = self.platform + else: + text = uri._replace(netloc="", scheme="") # remove platform + text = urlunparse(text)[1:] # combine to string and strip leading "/" return text diff --git a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/edit-this-page.html b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/edit-this-page.html index fcfbf08ab1..283618b50f 100644 --- a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/edit-this-page.html +++ b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/edit-this-page.html @@ -2,9 +2,9 @@ {% if sourcename is defined and theme_use_edit_page_button==true and page_source_suffix %} {% set src = sourcename.split('.') %}
- + {% set provider, url = get_edit_provider_and_url() %} + - {% set provider = get_edit_provider_and_url()[0] %} {% block edit_this_page_text %} {% if provider %} {% trans provider=provider %}Edit on {{ provider }}{% endtrans %} diff --git a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf index 30d18cc4ae..d0d72c53d3 100644 --- a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf +++ b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf @@ -14,6 +14,8 @@ external_links = bitbucket_url = github_url = gitlab_url = +forgejo_url = +gitea_url = twitter_url = icon_links_label = Icon Links icon_links = diff --git a/tests/sites/base/page1.rst b/tests/sites/base/page1.rst index ce3393abbd..634cbfa900 100644 --- a/tests/sites/base/page1.rst +++ b/tests/sites/base/page1.rst @@ -5,6 +5,64 @@ Page 1 - https://pydata-sphinx-theme.readthedocs.io/en/latest/ +**Codeberg** + +.. container:: codeberg-container + + https://codeberg.org + https://codeberg.org/codeberg-org + https://codeberg.org/codeberg-org/f-repo + https://codeberg.org/codeberg-org/f-repo/issues + https://codeberg.org/codeberg-org/f-repo/issues/ + https://codeberg.org/codeberg-org/f-repo/issues/42 + https://codeberg.org/codeberg-org/f-repo/pulls/ + https://codeberg.org/codeberg-org/f-repo/pulls/1012 + https://codeberg.org/codeberg-org/-/projects + https://codeberg.org/codeberg-org/-/projects/2 + https://codeberg.org/codeberg-org/f-repo/projects + https://codeberg.org/codeberg-org/f-repo/projects/1 + https://codeberg.org/codeberg-org/f-repo/link/to/unknown/feature + https://codeberg.org/explore/repos + +**Forgejo** + +.. container:: forgejo-container + + https://my-forgejo.org + https://my-forgejo.org/forgejo-org + https://my-forgejo.org/forgejo-org/f-repo + https://my-forgejo.org/forgejo-org/f-repo/issues + https://my-forgejo.org/forgejo-org/f-repo/issues/ + https://my-forgejo.org/forgejo-org/f-repo/issues/42 + https://my-forgejo.org/forgejo-org/f-repo/pulls/ + https://forgejo.my.org/forgejo-org/f-repo/pulls/1012 + https://forgejo.my.org/forgejo-org/-/projects + https://forgejo.my.org/forgejo-org/-/projects/2 + https://forgejo.my.org/forgejo-org/f-repo/projects + https://forgejo.my.org/forgejo-org/f-repo/projects/1 + https://forgejo.my.org/forgejo-org/f-repo/link/to/unknown/feature + https://forgejo.my.org/explore/repos + + +**Gitea** + +.. container:: gitea-container + + https://gitea.com + https://gitea.com/gitea-org + https://gitea.com/gitea-org/g-repo + https://gitea.com/gitea-org/g-repo/issues + https://gitea.com/gitea-org/g-repo/issues/ + https://gitea.com/gitea-org/g-repo/issues/42 + https://gitea.com/gitea-org/g-repo/pulls/ + https://gitea.com/gitea-org/g-repo/pulls/1012 + https://gitea.com/gitea-org/-/projects + https://gitea.com/gitea-org/-/projects/2 + https://gitea.com/gitea-org/g-repo/projects + https://gitea.com/gitea-org/g-repo/projects/1 + https://gitea.com/gitea-org/g-repo/link/to/unknown/feature + https://gitea.com/explore/repos + **GitHub** .. container:: github-container diff --git a/tests/test_build.py b/tests/test_build.py index f40a38acd2..e96a9dba6d 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -585,6 +585,44 @@ def test_footer(sphinx_build_factory) -> None: "https://bitbucket.org/foo/bar/src/HEAD/docs/index.rst?mode=edit", ), ], + [ + { + "forgejo_user": "foo", + "forgejo_repo": "bar", + "forgejo_version": "HEAD", + "doc_path": "docs", + "forgejo_url": "https://my-forgejo.com", + }, + ( + "Edit on Forgejo", + "https://my-forgejo.com/foo/bar/_edit/HEAD/docs/index.rst", + ), + ], + [ + { + "forgejo_user": "foo", + "forgejo_repo": "bar", + "forgejo_version": "HEAD", + "doc_path": "docs", + "forgejo_url": "https://codeberg.org", + }, + ( + "Edit on Codeberg", + "https://codeberg.org/foo/bar/_edit/HEAD/docs/index.rst", + ), + ], + [ + { + "gitea_user": "foo", + "gitea_repo": "bar", + "gitea_version": "HEAD", + "doc_path": "docs", + }, + ( + "Edit on Gitea", + "https://gitea.com/foo/bar/_edit/HEAD/docs/index.rst", + ), + ], ] @@ -608,16 +646,22 @@ def test_footer(sphinx_build_factory) -> None: dict( # copy all the values **html_context, - # add a provider url - **{f"{provider}_url": f"https://{provider}.example.com"}, + # add a provider url if not specified in html_context + **( + {f"{provider}_url": f"https://{provider}.example.com"} + if f"{provider}_url" not in html_context + else {} + ), ), ( text, - f"""https://{provider}.example.com/foo/{url.split("/foo/")[1]}""", + f"""https://{provider}.example.com/foo/{url.split("/foo/")[1]}""" + if f"{provider}_url" not in html_context + else f"{html_context[f'{provider}_url']}/foo/{url.split('/foo/')[1]}", ), ] for html_context, (text, url) in good_edits - for provider in ["github", "gitlab", "bitbucket"] + for provider in ["github", "gitlab", "bitbucket", "forgejo", "gitea"] if provider in text.lower() ] @@ -687,6 +731,10 @@ def test_edit_page_url(sphinx_build_factory, html_context, edit_text_and_url) -> "html_theme_options.use_edit_page_button": True, "html_context": html_context, } + if html_context.get("forgejo_url"): + confoverrides.update( + {"html_theme_options.forgejo_url": html_context["forgejo_url"]} + ) sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides) if edit_text_and_url is None: @@ -846,6 +894,19 @@ def test_shorten_link(sphinx_build_factory, file_regression) -> None: """Regression test for "edit on " link shortening.""" sphinx_build = sphinx_build_factory("base").build() + codeberg = sphinx_build.html_tree("page1.html").select(".codeberg-container")[0] + file_regression.check( + codeberg.prettify(), basename="codeberg_links", extension=".html" + ) + + forgejo = sphinx_build.html_tree("page1.html").select(".forgejo-container")[0] + file_regression.check( + forgejo.prettify(), basename="forgejo_links", extension=".html" + ) + + gitea = sphinx_build.html_tree("page1.html").select(".gitea-container")[0] + file_regression.check(gitea.prettify(), basename="gitea_links", extension=".html") + github = sphinx_build.html_tree("page1.html").select(".github-container")[0] file_regression.check(github.prettify(), basename="github_links", extension=".html") diff --git a/tests/test_build/codeberg_links.html b/tests/test_build/codeberg_links.html new file mode 100644 index 0000000000..e0e5aef0d2 --- /dev/null +++ b/tests/test_build/codeberg_links.html @@ -0,0 +1,46 @@ + diff --git a/tests/test_build/forgejo_links.html b/tests/test_build/forgejo_links.html new file mode 100644 index 0000000000..7f3ff90114 --- /dev/null +++ b/tests/test_build/forgejo_links.html @@ -0,0 +1,46 @@ + diff --git a/tests/test_build/gitea_links.html b/tests/test_build/gitea_links.html new file mode 100644 index 0000000000..3611780a54 --- /dev/null +++ b/tests/test_build/gitea_links.html @@ -0,0 +1,46 @@ + diff --git a/tests/test_edit.py b/tests/test_edit.py new file mode 100644 index 0000000000..6ad9db70dd --- /dev/null +++ b/tests/test_edit.py @@ -0,0 +1,70 @@ +"""Edit button unit tests.""" + +import pytest + +from pydata_sphinx_theme.edit_this_page import adjust_forge_params + + +@pytest.fixture +def default_forge_urls(): + """Setup default edit URLs.""" + return { + "forgejo_url": "https://codeberg.org", + "gitea_url": "https://gitea.com", + "gitlab_url": "https://gitlab.com", + } + + +@pytest.mark.parametrize( + "html_theme_options,modified_urls", + [ + ( + {"forgejo_url": "https://my-forgejo.com/f-proj/f-repo"}, + {"forgejo_url": "https://my-forgejo.com"}, + ), + ( + {"gitea_url": "https://my-gitea.com/g-proj/g-repo"}, + {"gitea_url": "https://my-gitea.com"}, + ), + ( + { + "gitlab_url": "https://my-gitlab.org/gl-proj/gl-repo", + "forgejo_url": "https://my-forgejo.com/f-proj/f-repo", + }, + { + "forgejo_url": "https://my-forgejo.com", + "gitlab_url": "https://my-gitlab.org", + }, + ), + ], +) +def test_adjust_forge_params_replace_urls( + default_forge_urls, html_theme_options, modified_urls +): + """Unit test for adjust_forge_params() url replacing.""" + forge_urls, forge_labels = adjust_forge_params( + default_forge_urls, {}, html_theme_options + ) + expected_urls = default_forge_urls | modified_urls + + assert forge_urls == expected_urls + assert forge_labels == {} + + +@pytest.mark.parametrize( + "html_theme_options,expected_labels", + [ + ({"forgejo_url": "https://noreplace.com"}, {}), + ({"forgejo_url": "https://codeberg.org"}, {"forgejo": "Codeberg"}), + ], +) +def test_adjust_forge_params_relabel( + default_forge_urls, html_theme_options, expected_labels +): + """Unit test for adjust_forge_params() relabeling.""" + forge_urls, forge_labels = adjust_forge_params( + default_forge_urls, {}, html_theme_options + ) + + assert forge_urls == default_forge_urls + assert forge_labels == expected_labels diff --git a/tests/test_short_url.py b/tests/test_short_url.py index e5f92bdc83..7c2de68a8e 100644 --- a/tests/test_short_url.py +++ b/tests/test_short_url.py @@ -89,6 +89,54 @@ class Mock: "https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6788", "gitlab-com/gl-infra/production#6788", ), + # codeberg + ( + "codeberg", + "https://codeberg.org/", + "codeberg", + ), + ( + "codeberg", + "https://codeberg.org/f-org/f-proj/issues/42", + "f-org/f-proj#42", + ), + # forgejo + ( + "forgejo", + "https://my-forgejo.com/", + "forgejo", + ), + ( + "forgejo", + "https://forgejo-host.org/f-org/f-proj/issues/42", + "f-org/f-proj#42", + ), + ( + "forgejo", + "https://forgejo-host.org/f-org/f-proj/pulls/43", + "f-org/f-proj#43", + ), + # gitea + ( + "gitea", + "https://gitea.com", + "gitea", + ), + ( + "gitea", + "https://my-gitea.com/", + "gitea", + ), + ( + "gitea", + "https://gitea.com/gitea-org/g-proj/issues/42", + "gitea-org/g-proj#42", + ), + ( + "gitea", + "https://gitea.com/gitea-org/g-proj/pulls/43", + "gitea-org/g-proj#43", + ), ], ) def test_shorten(platform, url, expected): @@ -107,3 +155,73 @@ def test_shorten(platform, url, expected): URI = urlparse(url) assert sl.parse_url(URI) == expected + + +@pytest.fixture(scope="session") +def shortener(): + """Setup ShortenLinkTransform object for testing.""" + document = Mock() + document.settings = Mock() + document.settings.language_code = "en" + document.reporter = None + return ShortenLinkTransform(document) + + +@pytest.mark.parametrize( + "url,html_options,expected", + [ + # forgejo + ( + "https://forgejo-instance.com/f-org/f-proj/pulls/43", + {}, + "forgejo", + ), + ( + "https://forgejo-instance.com/f-org/", + {}, + None, + ), + ( + "https://forgejo-instance.com/f-org/", + {"forgejo_url": "https://forgejo-instance.com/f-org/f-proj"}, + "forgejo", + ), + # gitea + ( + "https://gitea-instance.com/g-org/g-proj/pulls/43", + {}, + "gitea", + ), + ( + "https://gitea-instance.com/g-org/", + {}, + None, + ), + ( + "https://gitea-instance.com/g-org/", + {"gitea_url": "https://gitea-instance.com/g-org/g-proj"}, + "gitea", + ), + # gitlab + ( + "https://gitlab-instance.com/g-org/g-proj/-/merge_requests/43", + {}, + "gitlab", + ), + ( + "https://gitlab-instance.com/g-org/", + {}, + None, + ), + ( + "https://gitlab-instance.com/g-org/", + {"gitlab_url": "https://gitlab-instance.com/g-org/g-proj"}, + "gitlab", + ), + ], +) +def test_identify_selfhosted(url, html_options, expected, shortener): + """Unit test for self-hosted forges identification.""" + url = urlparse(url) + + assert shortener.identify_selfhosted(url, html_options) == expected