From e786e0da780195b88799ec49590a146f90e40f3c Mon Sep 17 00:00:00 2001 From: Matthew Pitkin Date: Tue, 3 Dec 2024 14:28:25 +0000 Subject: [PATCH 01/16] __init__.py: allow gitlab URL link shortening from non-gitlab.com domains --- src/pydata_sphinx_theme/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 3c613831ac..3ea7e75b32 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -276,7 +276,23 @@ def setup(app: Sphinx) -> Dict[str, str]: app.add_html_theme("pydata_sphinx_theme", str(theme_path)) - app.add_post_transform(short_link.ShortenLinkTransform) + if hasattr(app.config, "html_context"): + gitlab_url = app.config.html_context.get("gitlab_url", "") + + if gitlab_url.startswith("https://"): + gitlab_url = {gitlab_url[8:].rstrip("/"): "gitlab"} + elif gitlab_url.startswith("http://"): + gitlab_url = {gitlab_url[7:].rstrip("/"): "gitlab"} + else: + gitlab_url = {} + + class ShortenLinkTransformCustom(short_link.ShortenLinkTransform): + supported_platform = short_link.ShortenLinkTransform.supported_platform + supported_platform.update(gitlab_url) + + app.add_post_transform(ShortenLinkTransformCustom) + else: + app.add_post_transform(short_link.ShortenLinkTransform) app.connect("builder-inited", translator.setup_translators) app.connect("builder-inited", update_config) From 0e435853ae15f8f1483c4970abbd263dffe4a9fd Mon Sep 17 00:00:00 2001 From: Matthew Pitkin Date: Wed, 4 Dec 2024 14:31:59 +0000 Subject: [PATCH 02/16] __init__.py: also allow for non-github.com github domains --- src/pydata_sphinx_theme/__init__.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 3ea7e75b32..a4f40c66b0 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -277,18 +277,19 @@ def setup(app: Sphinx) -> Dict[str, str]: app.add_html_theme("pydata_sphinx_theme", str(theme_path)) if hasattr(app.config, "html_context"): - gitlab_url = app.config.html_context.get("gitlab_url", "") + github_url = app.config.html_content.get("github_url", None) + gitlab_url = app.config.html_context.get("gitlab_url", None) - if gitlab_url.startswith("https://"): - gitlab_url = {gitlab_url[8:].rstrip("/"): "gitlab"} - elif gitlab_url.startswith("http://"): - gitlab_url = {gitlab_url[7:].rstrip("/"): "gitlab"} - else: - gitlab_url = {} + url_update = {} + for url, platform in zip([github_url, gitlab_url], ["github", "gitlab"]): + if url: + # remove "http[s]://" and leading/trailing "/"s + url = urlparse(url)._replace(scheme="").geturl().lstrip("/").rstrip("/") + url_update[url] = platform class ShortenLinkTransformCustom(short_link.ShortenLinkTransform): supported_platform = short_link.ShortenLinkTransform.supported_platform - supported_platform.update(gitlab_url) + supported_platform.update(url_update) app.add_post_transform(ShortenLinkTransformCustom) else: From 9f9df680eae6786bcd75713287077bce3fd7f360 Mon Sep 17 00:00:00 2001 From: Matthew Pitkin Date: Wed, 4 Dec 2024 14:55:26 +0000 Subject: [PATCH 03/16] Add ability to shorten bitbucket links --- .../assets/styles/base/_base.scss | 7 ++++++- .../assets/styles/variables/_icons.scss | 1 + src/pydata_sphinx_theme/short_link.py | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/pydata_sphinx_theme/assets/styles/base/_base.scss b/src/pydata_sphinx_theme/assets/styles/base/_base.scss index 5ebd8bd06e..e76776e06e 100644 --- a/src/pydata_sphinx_theme/assets/styles/base/_base.scss +++ b/src/pydata_sphinx_theme/assets/styles/base/_base.scss @@ -48,7 +48,8 @@ a { // set up a icon next to the shorten links from github and gitlab &.github, - &.gitlab { + &.gitlab, + &.bitbucket { &::before { color: var(--pst-color-text-muted); font: var(--fa-font-brands); @@ -63,6 +64,10 @@ a { &.gitlab::before { content: var(--pst-icon-gitlab); } + + &.bitbucket::before { + content: var(--pst-icon-bitbucket); + } } %heading-style { diff --git a/src/pydata_sphinx_theme/assets/styles/variables/_icons.scss b/src/pydata_sphinx_theme/assets/styles/variables/_icons.scss index f7618ec4ed..9db8fff4ed 100644 --- a/src/pydata_sphinx_theme/assets/styles/variables/_icons.scss +++ b/src/pydata_sphinx_theme/assets/styles/variables/_icons.scss @@ -20,6 +20,7 @@ html { --pst-icon-search-minus: "\f010"; // fa-solid fa-magnifying-glass-minus --pst-icon-github: "\f09b"; // fa-brands fa-github --pst-icon-gitlab: "\f296"; // fa-brands fa-gitlab + --pst-icon-bitbucket: "\f171"; // fa-brands fa-bitbucket --pst-icon-share: "\f064"; // fa-solid fa-share --pst-icon-bell: "\f0f3"; // fa-solid fa-bell --pst-icon-pencil: "\f303"; // fa-solid fa-pencil diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index 34db161e49..10adffba4f 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -12,8 +12,8 @@ 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 link when they are coming from github, gitlab, or bitbucket and add + an extra class to the tag for further styling. Before: .. code-block:: html @@ -37,6 +37,7 @@ class ShortenLinkTransform(SphinxPostTransform): supported_platform: ClassVar[dict[str, str]] = { "github.com": "github", "gitlab.com": "gitlab", + "bitbucket.org": "bitbucket", } platform = None @@ -96,6 +97,19 @@ def parse_url(self, uri: ParseResult) -> str: if parts[2] in ["issues", "pull", "discussions"]: text += f"#{parts[-1]}" # element number + elif self.platform == "bitbucket": + # split the url content + parts = path.split("/") + + if len(parts) > 0: + text = parts[0] # organisation + if len(parts) > 1: + text += f"/{parts[1]}" # repository + if len(parts) > 2: + if parts[2] in ["issues", "pull-requests"]: + itemnumber = parts[3] + text += f"#{itemnumber}" # element number + elif self.platform == "gitlab": # cp. https://docs.gitlab.com/ee/user/markdown.html#gitlab-specific-references if "/-/" in path and any( From 617b97781a69a443019081661041b3693d4a94db Mon Sep 17 00:00:00 2001 From: Matthew Pitkin Date: Wed, 4 Dec 2024 14:57:31 +0000 Subject: [PATCH 04/16] __init__.py: add custum bitbucket url --- src/pydata_sphinx_theme/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index a4f40c66b0..992718334f 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -279,9 +279,12 @@ def setup(app: Sphinx) -> Dict[str, str]: if hasattr(app.config, "html_context"): github_url = app.config.html_content.get("github_url", None) gitlab_url = app.config.html_context.get("gitlab_url", None) + bitbucket_url = app.config.html_content.get("bitbucket_url", None) url_update = {} - for url, platform in zip([github_url, gitlab_url], ["github", "gitlab"]): + for url, platform in zip( + [github_url, gitlab_url, bitbucket_url], ["github", "gitlab", "bitbucket"] + ): if url: # remove "http[s]://" and leading/trailing "/"s url = urlparse(url)._replace(scheme="").geturl().lstrip("/").rstrip("/") From 277730fadbed61adde275e02e2a27fbc0103383f Mon Sep 17 00:00:00 2001 From: Matthew Pitkin Date: Wed, 4 Dec 2024 15:07:19 +0000 Subject: [PATCH 05/16] __init__.py: fix typo html_content -> html_context --- src/pydata_sphinx_theme/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index 992718334f..e371faf577 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -277,9 +277,9 @@ def setup(app: Sphinx) -> Dict[str, str]: app.add_html_theme("pydata_sphinx_theme", str(theme_path)) if hasattr(app.config, "html_context"): - github_url = app.config.html_content.get("github_url", None) + github_url = app.config.html_context.get("github_url", None) gitlab_url = app.config.html_context.get("gitlab_url", None) - bitbucket_url = app.config.html_content.get("bitbucket_url", None) + bitbucket_url = app.config.html_context.get("bitbucket_url", None) url_update = {} for url, platform in zip( From 86dd4181fcecb7a48014be63befd7d7c8a0a06e8 Mon Sep 17 00:00:00 2001 From: Matthew Pitkin Date: Wed, 4 Dec 2024 16:58:16 +0000 Subject: [PATCH 06/16] short_link.py: treatmemt for bitbucket workspace/overview URLs --- src/pydata_sphinx_theme/short_link.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index 10adffba4f..bd55955d6e 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -103,12 +103,15 @@ def parse_url(self, uri: ParseResult) -> str: if len(parts) > 0: text = parts[0] # organisation - if len(parts) > 1: - text += f"/{parts[1]}" # repository - if len(parts) > 2: - if parts[2] in ["issues", "pull-requests"]: - itemnumber = parts[3] - text += f"#{itemnumber}" # element number + if len(parts) > 1 and not ( + parts[-2] == "workspace" and parts[-1] == "overview" + ): + if len(parts) > 1: + text += f"/{parts[1]}" # repository + if len(parts) > 2: + if parts[2] in ["issues", "pull-requests"]: + itemnumber = parts[3] + text += f"#{itemnumber}" # element number elif self.platform == "gitlab": # cp. https://docs.gitlab.com/ee/user/markdown.html#gitlab-specific-references From 96a1812f3ca18c260d7a247c32f202b8e2e8e989 Mon Sep 17 00:00:00 2001 From: Matthew Pitkin Date: Wed, 4 Dec 2024 16:58:32 +0000 Subject: [PATCH 07/16] Update documentation --- docs/user_guide/source-buttons.rst | 2 ++ docs/user_guide/theme-elements.md | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/user_guide/source-buttons.rst b/docs/user_guide/source-buttons.rst index a4db469adf..249ca8454b 100644 --- a/docs/user_guide/source-buttons.rst +++ b/docs/user_guide/source-buttons.rst @@ -4,6 +4,8 @@ Source Buttons Source buttons are links to the source of your page's content (either on your site, or on hosting sites like GitHub). +.. _add-edit-button: + Add an edit button ================== diff --git a/docs/user_guide/theme-elements.md b/docs/user_guide/theme-elements.md index 3e54a750df..43936db779 100644 --- a/docs/user_guide/theme-elements.md +++ b/docs/user_guide/theme-elements.md @@ -212,7 +212,7 @@ All will end up as numbers in the rendered HTML, but in the source they look lik ## Link shortening for git repository services -Many projects have links back to their issues / PRs hosted on platforms like **GitHub** or **GitLab**. +Many projects have links back to their issues / PRs hosted on platforms like **GitHub**, **GitLab**, or **Bitbucket**. Instead of displaying these as raw links, this theme does some lightweight formatting for these platforms specifically. In **reStructuredText**, URLs are automatically converted to links, so this works automatically. @@ -252,5 +252,25 @@ There are a variety of link targets supported, here's a table for reference: - `https://gitlab.com/gitlab-org`: https://gitlab.com/gitlab-org - `https://gitlab.com/gitlab-org/gitlab`: https://gitlab.com/gitlab-org/gitlab - `https://gitlab.com/gitlab-org/gitlab/-/issues/375583`: https://gitlab.com/gitlab-org/gitlab/-/issues/375583 +- `https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174667`: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174667 + +**Bitbucket** + +- `https://bitbucket.org`: https://bitbucket.org +- `https://bitbucket.org/atlassian/workspace/overview`: https://bitbucket.org/atlassian/workspace/overview +- `https://bitbucket.org/atlassian/aui`: https://bitbucket.org/atlassian/aui +- `https://bitbucket.org/atlassian/aui/pull-requests/4758`: https://bitbucket.org/atlassian/aui/pull-requests/4758 Links provided with a text body won't be changed. + +If you have links to GitHub, GitLab, or Bitbucket repository URLs that are on non-standard domains +(i.e., not on `github.com`, `gitlab.com`, or `bitbucket.org`, respectively), then these will be +shortened if the base URL is given in the `html_context` section of your `conf.py` file (see +{ref}`Add an edit button `), e.g., + +```python +html_context = { + "gitlab_url": "https://gitlab.mydomain.com", # your self-hosted GitLab + ... +} +``` From 82339e36511edce287e27a7852ad089fb47753f3 Mon Sep 17 00:00:00 2001 From: gabalafou Date: Tue, 17 Jun 2025 14:52:26 +0200 Subject: [PATCH 08/16] add tests, TODO comments --- src/pydata_sphinx_theme/short_link.py | 2 +- tests/sites/base/page1.rst | 9 ++++ tests/test_build.py | 5 ++ tests/test_build/bitbucket_links.html | 16 ++++++ tests/test_short_url.py | 77 +++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 tests/test_build/bitbucket_links.html diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index bd55955d6e..c039f9d161 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -110,7 +110,7 @@ def parse_url(self, uri: ParseResult) -> str: text += f"/{parts[1]}" # repository if len(parts) > 2: if parts[2] in ["issues", "pull-requests"]: - itemnumber = parts[3] + itemnumber = parts[-1] text += f"#{itemnumber}" # element number elif self.platform == "gitlab": diff --git a/tests/sites/base/page1.rst b/tests/sites/base/page1.rst index ce3393abbd..304d87968d 100644 --- a/tests/sites/base/page1.rst +++ b/tests/sites/base/page1.rst @@ -31,3 +31,12 @@ Page 1 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84669 https://gitlab.com/gitlab-org/gitlab/-/pipelines/511894707 https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6788 + +**Bitbucket** + +.. container:: bitbucket-container + + https://bitbucket.org + https://bitbucket.org/atlassian/workspace/overview + https://bitbucket.org/atlassian/aui + https://bitbucket.org/atlassian/aui/pull-requests/4758 diff --git a/tests/test_build.py b/tests/test_build.py index f40a38acd2..a88cd15021 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -852,6 +852,11 @@ def test_shorten_link(sphinx_build_factory, file_regression) -> None: gitlab = sphinx_build.html_tree("page1.html").select(".gitlab-container")[0] file_regression.check(gitlab.prettify(), basename="gitlab_links", extension=".html") + bitbucket = sphinx_build.html_tree("page1.html").select(".bitbucket-container")[0] + file_regression.check( + bitbucket.prettify(), basename="bitbucket_links", extension=".html" + ) + def test_math_header_item(sphinx_build_factory, file_regression) -> None: """Regression test for math items in a header title.""" diff --git a/tests/test_build/bitbucket_links.html b/tests/test_build/bitbucket_links.html new file mode 100644 index 0000000000..6471767b51 --- /dev/null +++ b/tests/test_build/bitbucket_links.html @@ -0,0 +1,16 @@ + diff --git a/tests/test_short_url.py b/tests/test_short_url.py index e5f92bdc83..b79ac2fa9e 100644 --- a/tests/test_short_url.py +++ b/tests/test_short_url.py @@ -31,6 +31,19 @@ class Mock: "https://github.com/pydata/pydata-sphinx-theme/pull/1012", "pydata/pydata-sphinx-theme#1012", ), + ( + "github", + "https://github.com/pydata/pydata-sphinx-theme/issues", + # TODO: this is wrong + "pydata/pydata-sphinx-theme#issues", + ), + ( + # TODO: should this be shortened the way that GitHub does it: + # pydata/pydata-sphinx-theme@3caf346 + "github", + "https://github.com/pydata/pydata-sphinx-theme/commit/3caf346cacd2dad2a192a83c6cc9f8852e5a722e", + "pydata/pydata-sphinx-theme", + ), # TODO, I belive this is wrong as both orgs/pydata/projects/2 and # pydata/projects/issue/2 shorten to the same ("github", "https://github.com/orgs/pydata/projects/2", "pydata/projects#2"), @@ -89,6 +102,70 @@ class Mock: "https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6788", "gitlab-com/gl-infra/production#6788", ), + # Bitbucket + ("bitbucket", "https://bitbucket.org", "bitbucket"), + ("bitbucket", "https://bitbucket.org/atlassian", "atlassian"), + ( + "bitbucket", + "https://bitbucket.org/atlassian/workspace/overview", + "atlassian", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui", + "atlassian/aui", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/", + "atlassian/aui", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/pull-requests/4758", + "atlassian/aui#4758", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/issues/375583", + "atlassian/aui#375583", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/issues", + # TODO: this is wrong + "atlassian/aui#issues", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/issues/", + # TODO: this is wrong + "atlassian/aui#", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/commits/de41ded719e579d0ed4ffb8a81c29bb9ada10011", + # TODO: this is wrong, unknown patterns should just flow through unchanged + "atlassian/aui", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/branch/future/10.0.x", + # TODO: this is wrong, unknown patterns should just flow through unchanged + "atlassian/aui", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/pipelines", + # TODO: this is wrong, known patterns should just flow through unchanged + "atlassian/aui", + ), + ( + "bitbucket", + "https://bitbucket.org/atlassian/aui/pipelines/results/14542", + # TODO: this is wrong, known patterns should just flow through unchanged + "atlassian/aui", + ), ], ) def test_shorten(platform, url, expected): From 8158d88e2f30e4e0c862dac573247f19e61dce9f Mon Sep 17 00:00:00 2001 From: gabalafou Date: Tue, 17 Jun 2025 20:14:22 +0200 Subject: [PATCH 09/16] class method to configure ShortenLinkTransform, more tests --- src/pydata_sphinx_theme/__init__.py | 50 ++++---- src/pydata_sphinx_theme/short_link.py | 5 + .../sites/self_hosted_version_control/conf.py | 22 ++++ .../self_hosted_version_control/index.rst | 13 ++ .../self_hosted_version_control/links.rst | 42 +++++++ tests/test_build.py | 27 ++++ .../self_hosted_version_control_links.html | 117 ++++++++++++++++++ 7 files changed, 255 insertions(+), 21 deletions(-) create mode 100644 tests/sites/self_hosted_version_control/conf.py create mode 100644 tests/sites/self_hosted_version_control/index.rst create mode 100644 tests/sites/self_hosted_version_control/links.rst create mode 100644 tests/test_build/self_hosted_version_control_links.html diff --git a/src/pydata_sphinx_theme/__init__.py b/src/pydata_sphinx_theme/__init__.py index c2d0c20eae..5b4b25ec92 100644 --- a/src/pydata_sphinx_theme/__init__.py +++ b/src/pydata_sphinx_theme/__init__.py @@ -275,6 +275,33 @@ def _fix_canonical_url( context["pageurl"] = app.config.html_baseurl + target +def _add_self_hosted_platforms_to_link_transform_class(app: Sphinx) -> None: + if not hasattr(app.config, "html_context"): + return + + # Use list() to force the iterator to completion because the for-loop below + # can modify the dictionary. + platforms = list(short_link.ShortenLinkTransform.supported_platform.values()) + + for platform in platforms: + # {platform}_url -- e.g.: github_url, gitlab_url, bitbucket_url + self_hosted_url = app.config.html_context.get(f"{platform}_url", None) + if self_hosted_url is None: + continue + parsed = urlparse(self_hosted_url) + if parsed.scheme not in ("http", "https"): + raise Exception( + f"If you provide a value for html_context option {platform}_url," + " it must begin with http or https." + ) + if not parsed.netloc: + raise Exception( + f"Unsupported URL provided for html_context option {platform}_url." + " Could not get domain (netloc) from ${self_hosted_url}." + ) + short_link.ShortenLinkTransform.add_platform_mapping(platform, parsed.netloc) + + def setup(app: Sphinx) -> Dict[str, str]: """Setup the Sphinx application.""" here = Path(__file__).parent.resolve() @@ -282,30 +309,11 @@ def setup(app: Sphinx) -> Dict[str, str]: app.add_html_theme("pydata_sphinx_theme", str(theme_path)) - if hasattr(app.config, "html_context"): - github_url = app.config.html_context.get("github_url", None) - gitlab_url = app.config.html_context.get("gitlab_url", None) - bitbucket_url = app.config.html_context.get("bitbucket_url", None) - - url_update = {} - for url, platform in zip( - [github_url, gitlab_url, bitbucket_url], ["github", "gitlab", "bitbucket"] - ): - if url: - # remove "http[s]://" and leading/trailing "/"s - url = urlparse(url)._replace(scheme="").geturl().lstrip("/").rstrip("/") - url_update[url] = platform - - class ShortenLinkTransformCustom(short_link.ShortenLinkTransform): - supported_platform = short_link.ShortenLinkTransform.supported_platform - supported_platform.update(url_update) - - app.add_post_transform(ShortenLinkTransformCustom) - else: - app.add_post_transform(short_link.ShortenLinkTransform) + app.add_post_transform(short_link.ShortenLinkTransform) app.connect("builder-inited", translator.setup_translators) app.connect("builder-inited", update_config) + app.connect("builder-inited", _add_self_hosted_platforms_to_link_transform_class) app.connect("html-page-context", _fix_canonical_url) app.connect("html-page-context", edit_this_page.setup_edit_url) app.connect("html-page-context", toctree.add_toctree_functions) diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index c039f9d161..ec5f1fb580 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -41,6 +41,11 @@ class ShortenLinkTransform(SphinxPostTransform): } platform = None + @classmethod + def add_platform_mapping(cls, platform, netloc): + """Add domain->platform mapping to class at run-time.""" + cls.supported_platform.update(dict([(netloc, platform)])) + def run(self, **kwargs): """Run the Transform object.""" matcher = NodeMatcher(nodes.reference) diff --git a/tests/sites/self_hosted_version_control/conf.py b/tests/sites/self_hosted_version_control/conf.py new file mode 100644 index 0000000000..e3186b47cf --- /dev/null +++ b/tests/sites/self_hosted_version_control/conf.py @@ -0,0 +1,22 @@ +"""Test conf file.""" + +# -- Project information ----------------------------------------------------- + +project = "Test Self Hosted Version Control URLs" +copyright = "2020, Pydata community" +author = "Pydata community" + +root_doc = "index" + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] +html_theme = "pydata_sphinx_theme" +html_context = { + "github_url": "https://github.pydata.org", + "gitlab_url": "https://gitlab.pydata.org", + "bitbucket_url": "https://bitbucket.pydata.org", +} diff --git a/tests/sites/self_hosted_version_control/index.rst b/tests/sites/self_hosted_version_control/index.rst new file mode 100644 index 0000000000..268664d9d0 --- /dev/null +++ b/tests/sites/self_hosted_version_control/index.rst @@ -0,0 +1,13 @@ +Test conversion of a self-hosted GitHub URL +=========================================== + +This test ensures that a site using PyData Sphinx Theme can set a self-hosted +version control URL via the theme options and then when the site is built, that +the URLs that go to that self-hosted version control domain will be properly +shortened (just like for github.com, gitlab.com, and bitbucket.org). + +.. toctree:: + :caption: My caption + :numbered: + + links diff --git a/tests/sites/self_hosted_version_control/links.rst b/tests/sites/self_hosted_version_control/links.rst new file mode 100644 index 0000000000..da7b95bb2e --- /dev/null +++ b/tests/sites/self_hosted_version_control/links.rst @@ -0,0 +1,42 @@ +Test Self Hosted Version Control URLs +===================================== + +**normal link** + +- https://pydata-sphinx-theme.readthedocs.io/en/latest/ + +**GitHub** + +.. container:: github-container + + https://github.pydata.org + https://github.pydata.org/pydata + https://github.pydata.org/pydata/pydata-sphinx-theme + https://github.pydata.org/pydata/pydata-sphinx-theme/pull/1012 + https://github.pydata.org/orgs/pydata/projects/2 + +**GitLab** + +.. container:: gitlab-container + + https://gitlab.pydata.org + https://gitlab.pydata.org/gitlab-org + https://gitlab.pydata.org/gitlab-org/gitlab + https://gitlab.pydata.org/gitlab-org/gitlab/-/issues/375583 + https://gitlab.pydata.org/gitlab-org/gitlab/issues/375583 + https://gitlab.pydata.org/gitlab-org/gitlab/-/issues/ + https://gitlab.pydata.org/gitlab-org/gitlab/issues/ + https://gitlab.pydata.org/gitlab-org/gitlab/-/issues + https://gitlab.pydata.org/gitlab-org/gitlab/issues + https://gitlab.pydata.org/gitlab-org/gitlab/-/merge_requests/84669 + https://gitlab.pydata.org/gitlab-org/gitlab/-/pipelines/511894707 + https://gitlab.pydata.org/gitlab-com/gl-infra/production/-/issues/6788 + +**Bitbucket** + +.. container:: bitbucket-container + + https://bitbucket.pydata.org + https://bitbucket.pydata.org/atlassian/workspace/overview + https://bitbucket.pydata.org/atlassian/aui + https://bitbucket.pydata.org/atlassian/aui/pull-requests/4758 diff --git a/tests/test_build.py b/tests/test_build.py index a88cd15021..9d39cc4e52 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -858,6 +858,33 @@ def test_shorten_link(sphinx_build_factory, file_regression) -> None: ) +def test_self_hosted_shorten_link(sphinx_build_factory, file_regression) -> None: + """Check that self-hosted version control URLs get shortened. + + Example: + conf.py + html_context = {"github_url": "https://github.example.com"} + + example_page.rst + + In https://github.example.com/pydata/pydata-sphinx-theme/pull/101, + we refactored stylesheets and updated typography. + + example_page.html + + In + pydata/pydata-sphinx-theme#101, we refactored stylesheets and + updated typography. + """ + sphinx_build = sphinx_build_factory("self_hosted_version_control").build() + urls_page = sphinx_build.html_tree("links.html").select("article")[0] + file_regression.check( + urls_page.prettify(), + basename="self_hosted_version_control_links", + extension=".html", + ) + + def test_math_header_item(sphinx_build_factory, file_regression) -> None: """Regression test for math items in a header title.""" sphinx_build = sphinx_build_factory("base").build() diff --git a/tests/test_build/self_hosted_version_control_links.html b/tests/test_build/self_hosted_version_control_links.html new file mode 100644 index 0000000000..54443cc821 --- /dev/null +++ b/tests/test_build/self_hosted_version_control_links.html @@ -0,0 +1,117 @@ +
+
+

+ + 1. + + Test Self Hosted Version Control URLs + + # + +

+

+ + normal link + +

+ +

+ + GitHub + +

+ +

+ + GitLab + +

+ +

+ + Bitbucket + +

+ +
+
From 0924968b81dad45423369138490df67cef92ecc0 Mon Sep 17 00:00:00 2001 From: Matt Pitkin Date: Wed, 18 Jun 2025 10:50:49 +0100 Subject: [PATCH 10/16] Update src/pydata_sphinx_theme/short_link.py Co-authored-by: Daniel McCloy --- src/pydata_sphinx_theme/short_link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index ec5f1fb580..00d0b61b9a 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -44,7 +44,7 @@ class ShortenLinkTransform(SphinxPostTransform): @classmethod def add_platform_mapping(cls, platform, netloc): """Add domain->platform mapping to class at run-time.""" - cls.supported_platform.update(dict([(netloc, platform)])) + cls.supported_platform.update({netloc: platform}) def run(self, **kwargs): """Run the Transform object.""" From 13535e3fb12d2828554727471b2ad3a8368236d1 Mon Sep 17 00:00:00 2001 From: gabalafou Date: Tue, 24 Jun 2025 19:27:19 +0200 Subject: [PATCH 11/16] rewrite link shortener --- .pre-commit-config.yaml | 1 + src/pydata_sphinx_theme/short_link.py | 256 ++++++++++++------ tests/sites/base/page1.rst | 12 + tests/test_build/bitbucket_links.html | 12 +- tests/test_build/github_links.html | 38 ++- tests/test_build/gitlab_links.html | 34 +-- .../self_hosted_version_control_links.html | 62 ++--- tests/test_short_url.py | 113 ++++---- 8 files changed, 313 insertions(+), 215 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a78d98185b..e27aa1bf36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,6 +39,7 @@ repos: hooks: - id: djlint-jinja types_or: ["html"] + exclude: ^tests/test_build/.*\.html$ - repo: "https://github.com/PyCQA/doc8" rev: v1.1.2 diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index 00d0b61b9a..432fd518a4 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -1,7 +1,9 @@ """A custom Transform object to shorten github and gitlab links.""" +import re + from typing import ClassVar -from urllib.parse import ParseResult, urlparse, urlunparse +from urllib.parse import unquote, urlparse from docutils import nodes from sphinx.transforms.post_transforms import SphinxPostTransform @@ -39,7 +41,6 @@ class ShortenLinkTransform(SphinxPostTransform): "gitlab.com": "gitlab", "bitbucket.org": "bitbucket", } - platform = None @classmethod def add_platform_mapping(cls, platform, netloc): @@ -56,90 +57,169 @@ def run(self, **kwargs): # only act if the uri and text are the same # if not the user has already customized the display of the link if uri is not None and text is not None and text == uri: - uri = urlparse(uri) + parsed_uri = urlparse(uri) # only do something if the platform is identified - self.platform = self.supported_platform.get(uri.netloc) - if self.platform is not None: - node.attributes["classes"].append(self.platform) - node.children[0] = nodes.Text(self.parse_url(uri)) - - def parse_url(self, uri: ParseResult) -> str: - """Parse the content of the url with respect to the selected platform. - - Args: - uri: the link to the platform content - - Returns: - the reformated url title - """ - path = uri.path - if path == "": - # plain url passed, return platform only - return self.platform - - # if the path is not empty it contains a leading "/", which we don't want to - # include in the parsed content - path = path.lstrip("/") - - # check the platform name and read the information accordingly - # as "/#" - # or "//…//#" - if self.platform == "github": - # split the url content - parts = path.split("/") - - if parts[0] == "orgs" and "/projects" in path: - # We have a projects board link - # ref: `orgs/{org}/projects/{project-id}` - text = f"{parts[1]}/projects#{parts[3]}" - else: - # We have an issues, PRs, or repository link - if len(parts) > 0: - text = parts[0] # organisation - if len(parts) > 1: - text += f"/{parts[1]}" # repository - if len(parts) > 2: - if parts[2] in ["issues", "pull", "discussions"]: - text += f"#{parts[-1]}" # element number - - elif self.platform == "bitbucket": - # split the url content - parts = path.split("/") - - if len(parts) > 0: - text = parts[0] # organisation - if len(parts) > 1 and not ( - parts[-2] == "workspace" and parts[-1] == "overview" - ): - if len(parts) > 1: - text += f"/{parts[1]}" # repository - if len(parts) > 2: - if parts[2] in ["issues", "pull-requests"]: - itemnumber = parts[-1] - text += f"#{itemnumber}" # element number - - elif self.platform == "gitlab": - # cp. https://docs.gitlab.com/ee/user/markdown.html#gitlab-specific-references - if "/-/" in path and any( - map(uri.path.__contains__, ["issues", "merge_requests"]) - ): - group_and_subgroups, parts, *_ = path.split("/-/") - parts = parts.rstrip("/") - if "/" not in parts: - text = f"{group_and_subgroups}/{parts}" - else: - parts = parts.split("/") - url_type, element_number, *_ = parts - if not element_number: - text = group_and_subgroups - elif url_type == "issues": - text = f"{group_and_subgroups}#{element_number}" - elif url_type == "merge_requests": - text = f"{group_and_subgroups}!{element_number}" - else: - # display the whole uri (after "gitlab.com/") including parameters - # for example "///" - text = uri._replace(netloc="", scheme="") # remove platform - text = urlunparse(text)[1:] # combine to string and strip leading "/" - - return text + platform = self.supported_platform.get(parsed_uri.netloc) + if platform is not None: + short = shorten_url(platform, uri) + if short != uri: + node.attributes["classes"].append(platform) + node.children[0] = nodes.Text(short) + + +def shorten_url(platform: str, url: str) -> str: + """Parse the content of the path with respect to the selected platform. + + Args: + platform: "github", "gitlab", "bitbucket", etc. + url: the full url to the platform content, beginning with https:// + + Returns: + short form version of the url, + or the full url if it could not shorten it + """ + if platform == "github": + return shorten_github(url) + elif platform == "bitbucket": + return shorten_bitbucket(url) + elif platform == "gitlab": + return shorten_gitlab(url) + + return url + + +def shorten_github(url: str) -> str: + """ + Convert a GitHub URL to a short form like owner/repo#123 or + owner/repo@abc123. + """ + path = urlparse(url).path + + # Pull request URL + if match := re.match(r"/([^/]+)/([^/]+)/pull/(\d+)", path): + owner, repo, pr_id = match.groups() + return f"{owner}/{repo}#{pr_id}" + + # Issue URL + elif match := re.match(r"/([^/]+)/([^/]+)/issues/(\d+)", path): + owner, repo, issue_id = match.groups() + return f"{owner}/{repo}#{issue_id}" + + # Commit URL + elif match := re.match(r"/([^/]+)/([^/]+)/commit/([a-f0-9]{7,40})", path): + owner, repo, commit_hash = match.groups() + return f"{owner}/{repo}@{commit_hash[:7]}" + + # Branch URL + elif match := re.match(r"/([^/]+)/([^/]+)/tree/([^/]+)", path): + owner, repo, branch = match.groups() + return f"{owner}/{repo}:{unquote(branch)}" + + # Tag URL + elif match := re.match(r"/([^/]+)/([^/]+)/releases/tag/([^/]+)", path): + owner, repo, tag = match.groups() + return f"{owner}/{repo}@{unquote(tag)}" + + # File URL + elif match := re.match(r"/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", path): + owner, repo, ref, filepath = match.groups() + return f"{owner}/{repo}@{ref}/{unquote(filepath)}" + + # No match — return the original URL + return url + + +def shorten_gitlab(url: str) -> str: + """ + Convert a GitLab URL to a short form like group/project!123 or + group/project@abcdef7. + + Supports both canonical ('/-/') and non-canonical path formats. + """ + path = urlparse(url).path + + # Merge requests + if (m := re.match(r"^/(.+)/([^/]+)/-/merge_requests/(\d+)$", path)) or ( + m := re.match(r"^/(.+)/([^/]+)/merge_requests/(\d+)$", path) + ): + namespace, project, mr_id = m.groups() + return f"{namespace}/{project}!{mr_id}" + + # Issues + if (m := re.match(r"^/(.+)/([^/]+)/-/issues/(\d+)$", path)) or ( + m := re.match(r"^/(.+)/([^/]+)/issues/(\d+)$", path) + ): + namespace, project, issue_id = m.groups() + return f"{namespace}/{project}#{issue_id}" + + # Commits + if (m := re.match(r"^/(.+)/([^/]+)/-/commit/([a-fA-F0-9]+)$", path)) or ( + m := re.match(r"^/(.+)/([^/]+)/commit/([a-fA-F0-9]+)$", path) + ): + namespace, project, commit_hash = m.groups() + return f"{namespace}/{project}@{commit_hash[:7]}" + + # Branches (tree) + if (m := re.match(r"^https://gitlab\.com/(.+)/([^/]+)/-/tree/(.+)$", path)) or ( + m := re.match(r"^https://gitlab\.com/(.+)/([^/]+)/tree/(.+)$", path) + ): + namespace, project, branch = m.groups() + return f"{namespace}/{project}:{unquote(branch)}" + + # Tags + if (m := re.match(r"^/(.+)/([^/]+)/-/tags/(.+)$", path)) or ( + m := re.match(r"^/(.+)/([^/]+)/tags/(.+)$", path) + ): + namespace, project, tag = m.groups() + return f"{namespace}/{project}@{unquote(tag)}" + + # Blob (files) + if (m := re.match(r"^/(.+)/([^/]+)/-/blob/([^/]+)/(.+)$", path)) or ( + m := re.match(r"^/(.+)/([^/]+)/blob/([^/]+)/(.+)$", path) + ): + namespace, project, ref, path = m.groups() + return f"{namespace}/{project}@{ref}/{unquote(path)}" + + # No match — return the original URL + return url + + +def shorten_bitbucket(url: str) -> str: + """ + Convert a Bitbucket URL to a short form like team/repo#123 or + team/repo@main. + """ + path = urlparse(url).path + + # Pull request URL + if match := re.match(r"/([^/]+)/([^/]+)/pull-requests/(\d+)", path): + workspace, repo, pr_id = match.groups() + return f"{workspace}/{repo}#{pr_id}" + + # Issue URL + elif match := re.match(r"/([^/]+)/([^/]+)/issues/(\d+)", path): + workspace, repo, issue_id = match.groups() + return f"{workspace}/{repo}!{issue_id}" + + # Commit URL + elif match := re.match(r"/([^/]+)/([^/]+)/commits/([a-f0-9]+)", path): + workspace, repo, commit_hash = match.groups() + return f"{workspace}/{repo}@{commit_hash[:7]}" + + # Branch URL + elif match := re.match(r"/([^/]+)/([^/]+)/branch/(.+)", path): + workspace, repo, branch = match.groups() + return f"{workspace}/{repo}:{unquote(branch)}" + + # Tag URL + elif match := re.match(r"/([^/]+)/([^/]+)/commits/tag/(.+)", path): + workspace, repo, tag = match.groups() + return f"{workspace}/{repo}@{unquote(tag)}" + + # File URL + elif match := re.match(r"/([^/]+)/([^/]+)/src/([^/]+)/(.*)", path): + workspace, repo, ref, path = match.groups() + return f"{workspace}/{repo}@{ref}/{unquote(path)}" + + # No match — return the original URL + return url diff --git a/tests/sites/base/page1.rst b/tests/sites/base/page1.rst index 304d87968d..a96baf73d1 100644 --- a/tests/sites/base/page1.rst +++ b/tests/sites/base/page1.rst @@ -15,6 +15,18 @@ Page 1 https://github.com/pydata/pydata-sphinx-theme/pull/1012 https://github.com/orgs/pydata/projects/2 + http will get shortened: + + http://github.com/pydata/pydata-sphinx-theme/pull/1012 + + www will not get shortened: + + https://www.github.com/pydata/pydata-sphinx-theme/pull/1012 + + will not be linkified: + + github.com/pydata/pydata-sphinx-theme/pull/1012 + **GitLab** .. container:: gitlab-container diff --git a/tests/test_build/bitbucket_links.html b/tests/test_build/bitbucket_links.html index 6471767b51..229faef6fb 100644 --- a/tests/test_build/bitbucket_links.html +++ b/tests/test_build/bitbucket_links.html @@ -1,13 +1,13 @@

- - bitbucket + + https://bitbucket.org - - atlassian + + https://bitbucket.org/atlassian/workspace/overview - - atlassian/aui + + https://bitbucket.org/atlassian/aui atlassian/aui#4758 diff --git a/tests/test_build/github_links.html b/tests/test_build/github_links.html index 104637fc70..2a6a8b507a 100644 --- a/tests/test_build/github_links.html +++ b/tests/test_build/github_links.html @@ -1,19 +1,41 @@

diff --git a/tests/test_build/gitlab_links.html b/tests/test_build/gitlab_links.html index f12fcba43b..173f5a7008 100644 --- a/tests/test_build/gitlab_links.html +++ b/tests/test_build/gitlab_links.html @@ -1,37 +1,37 @@

- - gitlab + + https://gitlab.com - - gitlab-org + + https://gitlab.com/gitlab-org - - gitlab-org/gitlab + + https://gitlab.com/gitlab-org/gitlab gitlab-org/gitlab#375583 - gitlab-org/gitlab/issues/375583 + gitlab-org/gitlab#375583 - - gitlab-org/gitlab/issues + + https://gitlab.com/gitlab-org/gitlab/-/issues/ - - gitlab-org/gitlab/issues/ + + https://gitlab.com/gitlab-org/gitlab/issues/ - - gitlab-org/gitlab/issues + + https://gitlab.com/gitlab-org/gitlab/-/issues - - gitlab-org/gitlab/issues + + https://gitlab.com/gitlab-org/gitlab/issues gitlab-org/gitlab!84669 - - gitlab-org/gitlab/-/pipelines/511894707 + + https://gitlab.com/gitlab-org/gitlab/-/pipelines/511894707 gitlab-com/gl-infra/production#6788 diff --git a/tests/test_build/self_hosted_version_control_links.html b/tests/test_build/self_hosted_version_control_links.html index 54443cc821..4c501d9602 100644 --- a/tests/test_build/self_hosted_version_control_links.html +++ b/tests/test_build/self_hosted_version_control_links.html @@ -30,20 +30,20 @@

@@ -54,38 +54,38 @@

- - gitlab + + https://gitlab.pydata.org - - gitlab-org + + https://gitlab.pydata.org/gitlab-org - - gitlab-org/gitlab + + https://gitlab.pydata.org/gitlab-org/gitlab gitlab-org/gitlab#375583 - gitlab-org/gitlab/issues/375583 + gitlab-org/gitlab#375583 - - gitlab-org/gitlab/issues + + https://gitlab.pydata.org/gitlab-org/gitlab/-/issues/ - - gitlab-org/gitlab/issues/ + + https://gitlab.pydata.org/gitlab-org/gitlab/issues/ - - gitlab-org/gitlab/issues + + https://gitlab.pydata.org/gitlab-org/gitlab/-/issues - - gitlab-org/gitlab/issues + + https://gitlab.pydata.org/gitlab-org/gitlab/issues gitlab-org/gitlab!84669 - - gitlab-org/gitlab/-/pipelines/511894707 + + https://gitlab.pydata.org/gitlab-org/gitlab/-/pipelines/511894707 gitlab-com/gl-infra/production#6788 @@ -99,14 +99,14 @@

- - bitbucket + + https://bitbucket.pydata.org - - atlassian + + https://bitbucket.pydata.org/atlassian/workspace/overview - - atlassian/aui + + https://bitbucket.pydata.org/atlassian/aui atlassian/aui#4758 diff --git a/tests/test_short_url.py b/tests/test_short_url.py index b79ac2fa9e..7e6ca90f03 100644 --- a/tests/test_short_url.py +++ b/tests/test_short_url.py @@ -1,30 +1,20 @@ """Shortening url tests.""" -from urllib.parse import urlparse - import pytest -from pydata_sphinx_theme.short_link import ShortenLinkTransform - - -class Mock: - """mock object.""" - - pass +from pydata_sphinx_theme.short_link import shorten_url @pytest.mark.parametrize( "platform,url,expected", [ - # TODO, I belive this is wrong as both github.com and github.com/github - # shorten to just github. - ("github", "https://github.com", "github"), - ("github", "https://github.com/github", "github"), - ("github", "https://github.com/pydata", "pydata"), + ("github", "https://github.com", "https://github.com"), + ("github", "https://github.com/github", "https://github.com/github"), + ("github", "https://github.com/pydata", "https://github.com/pydata"), ( "github", "https://github.com/pydata/pydata-sphinx-theme", - "pydata/pydata-sphinx-theme", + "https://github.com/pydata/pydata-sphinx-theme", ), ( "github", @@ -34,58 +24,63 @@ class Mock: ( "github", "https://github.com/pydata/pydata-sphinx-theme/issues", - # TODO: this is wrong - "pydata/pydata-sphinx-theme#issues", + "https://github.com/pydata/pydata-sphinx-theme/issues", ), ( - # TODO: should this be shortened the way that GitHub does it: - # pydata/pydata-sphinx-theme@3caf346 "github", "https://github.com/pydata/pydata-sphinx-theme/commit/3caf346cacd2dad2a192a83c6cc9f8852e5a722e", - "pydata/pydata-sphinx-theme", + "pydata/pydata-sphinx-theme@3caf346", + ), + ( + "github", + "https://github.com/orgs/pydata/projects/2", + "https://github.com/orgs/pydata/projects/2", ), - # TODO, I belive this is wrong as both orgs/pydata/projects/2 and - # pydata/projects/issue/2 shorten to the same - ("github", "https://github.com/orgs/pydata/projects/2", "pydata/projects#2"), ("github", "https://github.com/pydata/projects/pull/2", "pydata/projects#2"), - # issues and pulls are athe same, so it's ok to normalise to the same here + # issues and pulls are the same, so it's ok to normalise to the same here ("github", "https://github.com/pydata/projects/issues/2", "pydata/projects#2"), # Gitlab - ("gitlab", "https://gitlab.com/tezos/tezos/-/issues", "tezos/tezos/issues"), - ("gitlab", "https://gitlab.com/tezos/tezos/issues", "tezos/tezos/issues"), + ( + "gitlab", + "https://gitlab.com/tezos/tezos/-/issues", + "https://gitlab.com/tezos/tezos/-/issues", + ), + ( + "gitlab", + "https://gitlab.com/tezos/tezos/issues", + "https://gitlab.com/tezos/tezos/issues", + ), ( "gitlab", "https://gitlab.com/gitlab-org/gitlab/-/issues/375583", "gitlab-org/gitlab#375583", ), ( - # TODO, non canonical url, discuss if should maybe be shortened to - # gitlab-org/gitlab#375583 + # non canonical url "gitlab", "https://gitlab.com/gitlab-org/gitlab/issues/375583", - "gitlab-org/gitlab/issues/375583", + "gitlab-org/gitlab#375583", ), ( "gitlab", "https://gitlab.com/gitlab-org/gitlab/-/issues/", - "gitlab-org/gitlab/issues", + "https://gitlab.com/gitlab-org/gitlab/-/issues/", ), ( - # TODO, non canonical url, discuss if should maybe be shortened to - # gitlab-org/gitlab/issues (no trailing slash) + # non canonical url "gitlab", "https://gitlab.com/gitlab-org/gitlab/issues/", - "gitlab-org/gitlab/issues/", + "https://gitlab.com/gitlab-org/gitlab/issues/", ), ( "gitlab", "https://gitlab.com/gitlab-org/gitlab/-/issues", - "gitlab-org/gitlab/issues", + "https://gitlab.com/gitlab-org/gitlab/-/issues", ), ( "gitlab", "https://gitlab.com/gitlab-org/gitlab/issues", - "gitlab-org/gitlab/issues", + "https://gitlab.com/gitlab-org/gitlab/issues", ), ( "gitlab", @@ -95,7 +90,7 @@ class Mock: ( "gitlab", "https://gitlab.com/gitlab-org/gitlab/-/pipelines/511894707", - "gitlab-org/gitlab/-/pipelines/511894707", + "https://gitlab.com/gitlab-org/gitlab/-/pipelines/511894707", ), ( "gitlab", @@ -103,22 +98,26 @@ class Mock: "gitlab-com/gl-infra/production#6788", ), # Bitbucket - ("bitbucket", "https://bitbucket.org", "bitbucket"), - ("bitbucket", "https://bitbucket.org/atlassian", "atlassian"), + ("bitbucket", "https://bitbucket.org", "https://bitbucket.org"), + ( + "bitbucket", + "https://bitbucket.org/atlassian", + "https://bitbucket.org/atlassian", + ), ( "bitbucket", "https://bitbucket.org/atlassian/workspace/overview", - "atlassian", + "https://bitbucket.org/atlassian/workspace/overview", ), ( "bitbucket", "https://bitbucket.org/atlassian/aui", - "atlassian/aui", + "https://bitbucket.org/atlassian/aui", ), ( "bitbucket", "https://bitbucket.org/atlassian/aui/", - "atlassian/aui", + "https://bitbucket.org/atlassian/aui/", ), ( "bitbucket", @@ -128,43 +127,37 @@ class Mock: ( "bitbucket", "https://bitbucket.org/atlassian/aui/issues/375583", - "atlassian/aui#375583", + "atlassian/aui!375583", ), ( "bitbucket", "https://bitbucket.org/atlassian/aui/issues", - # TODO: this is wrong - "atlassian/aui#issues", + "https://bitbucket.org/atlassian/aui/issues", ), ( "bitbucket", "https://bitbucket.org/atlassian/aui/issues/", - # TODO: this is wrong - "atlassian/aui#", + "https://bitbucket.org/atlassian/aui/issues/", ), ( "bitbucket", "https://bitbucket.org/atlassian/aui/commits/de41ded719e579d0ed4ffb8a81c29bb9ada10011", - # TODO: this is wrong, unknown patterns should just flow through unchanged - "atlassian/aui", + "atlassian/aui@de41ded", ), ( "bitbucket", "https://bitbucket.org/atlassian/aui/branch/future/10.0.x", - # TODO: this is wrong, unknown patterns should just flow through unchanged - "atlassian/aui", + "atlassian/aui:future/10.0.x", ), ( "bitbucket", "https://bitbucket.org/atlassian/aui/pipelines", - # TODO: this is wrong, known patterns should just flow through unchanged - "atlassian/aui", + "https://bitbucket.org/atlassian/aui/pipelines", ), ( "bitbucket", "https://bitbucket.org/atlassian/aui/pipelines/results/14542", - # TODO: this is wrong, known patterns should just flow through unchanged - "atlassian/aui", + "https://bitbucket.org/atlassian/aui/pipelines/results/14542", ), ], ) @@ -173,14 +166,4 @@ def test_shorten(platform, url, expected): Usually you also want a build test in `test_build.py` """ - document = Mock() - document.settings = Mock() - document.settings.language_code = "en" - document.reporter = None - - sl = ShortenLinkTransform(document) - sl.platform = platform - - URI = urlparse(url) - - assert sl.parse_url(URI) == expected + assert shorten_url(platform, url) == expected From 394bf57b5a077b11680a012b958eccd9bfd7ace7 Mon Sep 17 00:00:00 2001 From: gabalafou Date: Tue, 24 Jun 2025 19:43:04 +0200 Subject: [PATCH 12/16] update docstring for test function --- tests/test_build.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/test_build.py b/tests/test_build.py index 9d39cc4e52..99b81cab9f 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -861,20 +861,24 @@ def test_shorten_link(sphinx_build_factory, file_regression) -> None: def test_self_hosted_shorten_link(sphinx_build_factory, file_regression) -> None: """Check that self-hosted version control URLs get shortened. - Example: - conf.py - html_context = {"github_url": "https://github.example.com"} + Before build: - example_page.rst + conf.py - In https://github.example.com/pydata/pydata-sphinx-theme/pull/101, - we refactored stylesheets and updated typography. + html_context = {"github_url": "https://github.example.com"} - example_page.html + example_page.rst - In - pydata/pydata-sphinx-theme#101, we refactored stylesheets and - updated typography. + In https://github.example.com/pydata/pydata-sphinx-theme/pull/101, + we refactored stylesheets and updated typography. + + After build: + + example_page.html + + In + pydata/pydata-sphinx-theme#101, we refactored stylesheets and + updated typography. """ sphinx_build = sphinx_build_factory("self_hosted_version_control").build() urls_page = sphinx_build.html_tree("links.html").select("article")[0] From 5a7cda5a5986849449bf3935ef5d8263e6510541 Mon Sep 17 00:00:00 2001 From: gabalafou Date: Thu, 26 Jun 2025 19:28:23 +0200 Subject: [PATCH 13/16] reduce link shortener to 3 types --- src/pydata_sphinx_theme/short_link.py | 115 +++++++++++--------------- 1 file changed, 46 insertions(+), 69 deletions(-) diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index 432fd518a4..bfdcc155a1 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -3,7 +3,7 @@ import re from typing import ClassVar -from urllib.parse import unquote, urlparse +from urllib.parse import urlparse from docutils import nodes from sphinx.transforms.post_transforms import SphinxPostTransform @@ -96,35 +96,29 @@ def shorten_github(url: str) -> str: path = urlparse(url).path # Pull request URL + # - Example: + # - https://github.com/pydata/pydata-sphinx-theme/pull/2068 + # - pydata/pydata-sphinx-theme#2068 if match := re.match(r"/([^/]+)/([^/]+)/pull/(\d+)", path): owner, repo, pr_id = match.groups() return f"{owner}/{repo}#{pr_id}" # Issue URL + # - Example: + # - https://github.com/pydata/pydata-sphinx-theme/issues/2176 + # - pydata/pydata-sphinx-theme#2176 elif match := re.match(r"/([^/]+)/([^/]+)/issues/(\d+)", path): owner, repo, issue_id = match.groups() return f"{owner}/{repo}#{issue_id}" # Commit URL - elif match := re.match(r"/([^/]+)/([^/]+)/commit/([a-f0-9]{7,40})", path): + # - Example: + # - https://github.com/pydata/pydata-sphinx-theme/commit/51af2a27e8a008d0b44ed9ea9b45311e686d12f7 + # - pydata/pydata-sphinx-theme@51af2a2 + elif match := re.match(r"/([^/]+)/([^/]+)/commit/([a-f0-9]+)", path): owner, repo, commit_hash = match.groups() return f"{owner}/{repo}@{commit_hash[:7]}" - # Branch URL - elif match := re.match(r"/([^/]+)/([^/]+)/tree/([^/]+)", path): - owner, repo, branch = match.groups() - return f"{owner}/{repo}:{unquote(branch)}" - - # Tag URL - elif match := re.match(r"/([^/]+)/([^/]+)/releases/tag/([^/]+)", path): - owner, repo, tag = match.groups() - return f"{owner}/{repo}@{unquote(tag)}" - - # File URL - elif match := re.match(r"/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", path): - owner, repo, ref, filepath = match.groups() - return f"{owner}/{repo}@{ref}/{unquote(filepath)}" - # No match — return the original URL return url @@ -139,47 +133,32 @@ def shorten_gitlab(url: str) -> str: path = urlparse(url).path # Merge requests - if (m := re.match(r"^/(.+)/([^/]+)/-/merge_requests/(\d+)$", path)) or ( - m := re.match(r"^/(.+)/([^/]+)/merge_requests/(\d+)$", path) - ): - namespace, project, mr_id = m.groups() + # - Example: + # - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195598 + # - gitlab-org/gitlab!195598 + if match := re.match(r"^/(.+)/([^/]+)/-/merge_requests/(\d+)$", path): + namespace, project, mr_id = match.groups() return f"{namespace}/{project}!{mr_id}" # Issues - if (m := re.match(r"^/(.+)/([^/]+)/-/issues/(\d+)$", path)) or ( - m := re.match(r"^/(.+)/([^/]+)/issues/(\d+)$", path) - ): - namespace, project, issue_id = m.groups() + # - Example: + # - https://gitlab.com/gitlab-org/gitlab/-/issues/551885 + # - gitlab-org/gitlab#195598 + # + # TODO: support hash URLs, for example: + # https://gitlab.com/gitlab-org/gitlab/-/issues/545699#note_2543533261 + if match := re.match(r"^/(.+)/([^/]+)/-/issues/(\d+)$", path): + namespace, project, issue_id = match.groups() return f"{namespace}/{project}#{issue_id}" # Commits - if (m := re.match(r"^/(.+)/([^/]+)/-/commit/([a-fA-F0-9]+)$", path)) or ( - m := re.match(r"^/(.+)/([^/]+)/commit/([a-fA-F0-9]+)$", path) - ): - namespace, project, commit_hash = m.groups() + # - Example: + # - https://gitlab.com/gitlab-org/gitlab/-/commit/81872624c4c58425a040e158fd228d8f0c2bda07 + # - gitlab-org/gitlab@8187262 + if match := re.match(r"^/(.+)/([^/]+)/-/commit/([a-f0-9]+)$", path): + namespace, project, commit_hash = match.groups() return f"{namespace}/{project}@{commit_hash[:7]}" - # Branches (tree) - if (m := re.match(r"^https://gitlab\.com/(.+)/([^/]+)/-/tree/(.+)$", path)) or ( - m := re.match(r"^https://gitlab\.com/(.+)/([^/]+)/tree/(.+)$", path) - ): - namespace, project, branch = m.groups() - return f"{namespace}/{project}:{unquote(branch)}" - - # Tags - if (m := re.match(r"^/(.+)/([^/]+)/-/tags/(.+)$", path)) or ( - m := re.match(r"^/(.+)/([^/]+)/tags/(.+)$", path) - ): - namespace, project, tag = m.groups() - return f"{namespace}/{project}@{unquote(tag)}" - - # Blob (files) - if (m := re.match(r"^/(.+)/([^/]+)/-/blob/([^/]+)/(.+)$", path)) or ( - m := re.match(r"^/(.+)/([^/]+)/blob/([^/]+)/(.+)$", path) - ): - namespace, project, ref, path = m.groups() - return f"{namespace}/{project}@{ref}/{unquote(path)}" - # No match — return the original URL return url @@ -192,34 +171,32 @@ def shorten_bitbucket(url: str) -> str: path = urlparse(url).path # Pull request URL - if match := re.match(r"/([^/]+)/([^/]+)/pull-requests/(\d+)", path): + # - Example: + # - https://bitbucket.org/atlassian/atlassian-jwt-js/pull-requests/23 + # - atlassian/atlassian-jwt-js#23 + if match := re.match(r"^/([^/]+)/([^/]+)/pull-requests/(\d+)$", path): workspace, repo, pr_id = match.groups() return f"{workspace}/{repo}#{pr_id}" - # Issue URL - elif match := re.match(r"/([^/]+)/([^/]+)/issues/(\d+)", path): + # Issue URL. + # - Example: + # - https://bitbucket.org/atlassian/atlassian-jwt-js/issues/11/ + # - atlassian/atlassian-jwt-js!11 + # + # Deliberately not matching the end of the string because sometimes + # Bitbucket issue URLs include a slug at the end, for example: + # https://bitbucket.org/atlassian/atlassian-jwt-js/issues/11/nested-object-properties-are-represented + elif match := re.match(r"^/([^/]+)/([^/]+)/issues/(\d+)", path): workspace, repo, issue_id = match.groups() return f"{workspace}/{repo}!{issue_id}" # Commit URL - elif match := re.match(r"/([^/]+)/([^/]+)/commits/([a-f0-9]+)", path): + # - Example: + # - https://bitbucket.org/atlassian/atlassian-jwt-js/commits/d9b5197f0aeedeabf9d0f8d0953a80be65743d8a + # - atlassian/atlassian-jwt-js@d9b5197 + elif match := re.match(r"^/([^/]+)/([^/]+)/commits/([a-f0-9]+)$", path): workspace, repo, commit_hash = match.groups() return f"{workspace}/{repo}@{commit_hash[:7]}" - # Branch URL - elif match := re.match(r"/([^/]+)/([^/]+)/branch/(.+)", path): - workspace, repo, branch = match.groups() - return f"{workspace}/{repo}:{unquote(branch)}" - - # Tag URL - elif match := re.match(r"/([^/]+)/([^/]+)/commits/tag/(.+)", path): - workspace, repo, tag = match.groups() - return f"{workspace}/{repo}@{unquote(tag)}" - - # File URL - elif match := re.match(r"/([^/]+)/([^/]+)/src/([^/]+)/(.*)", path): - workspace, repo, ref, path = match.groups() - return f"{workspace}/{repo}@{ref}/{unquote(path)}" - # No match — return the original URL return url From 6d58054de06279f28e6febbcb9eea4dba26e2d98 Mon Sep 17 00:00:00 2001 From: gabalafou Date: Thu, 26 Jun 2025 19:30:13 +0200 Subject: [PATCH 14/16] update unit tests --- tests/test_short_url.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_short_url.py b/tests/test_short_url.py index 7e6ca90f03..b3786dd4b0 100644 --- a/tests/test_short_url.py +++ b/tests/test_short_url.py @@ -56,10 +56,10 @@ "gitlab-org/gitlab#375583", ), ( - # non canonical url + # non canonical url - not supported "gitlab", "https://gitlab.com/gitlab-org/gitlab/issues/375583", - "gitlab-org/gitlab#375583", + "https://gitlab.com/gitlab-org/gitlab/issues/375583", ), ( "gitlab", @@ -147,7 +147,7 @@ ( "bitbucket", "https://bitbucket.org/atlassian/aui/branch/future/10.0.x", - "atlassian/aui:future/10.0.x", + "https://bitbucket.org/atlassian/aui/branch/future/10.0.x", ), ( "bitbucket", From a5d70868c625651d56fb551be99700e84a579d5d Mon Sep 17 00:00:00 2001 From: gabalafou Date: Thu, 26 Jun 2025 19:32:03 +0200 Subject: [PATCH 15/16] update fixtures --- tests/test_build/gitlab_links.html | 4 ++-- tests/test_build/self_hosted_version_control_links.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_build/gitlab_links.html b/tests/test_build/gitlab_links.html index 173f5a7008..4a36fe4132 100644 --- a/tests/test_build/gitlab_links.html +++ b/tests/test_build/gitlab_links.html @@ -12,8 +12,8 @@ gitlab-org/gitlab#375583 - - gitlab-org/gitlab#375583 + + https://gitlab.com/gitlab-org/gitlab/issues/375583 https://gitlab.com/gitlab-org/gitlab/-/issues/ diff --git a/tests/test_build/self_hosted_version_control_links.html b/tests/test_build/self_hosted_version_control_links.html index 4c501d9602..b51ab7bc50 100644 --- a/tests/test_build/self_hosted_version_control_links.html +++ b/tests/test_build/self_hosted_version_control_links.html @@ -66,8 +66,8 @@

gitlab-org/gitlab#375583 - - gitlab-org/gitlab#375583 + + https://gitlab.pydata.org/gitlab-org/gitlab/issues/375583 https://gitlab.pydata.org/gitlab-org/gitlab/-/issues/ From b55a80e99c77a9ffc43d59652abce3a310bb129d Mon Sep 17 00:00:00 2001 From: gabalafou Date: Thu, 26 Jun 2025 19:33:49 +0200 Subject: [PATCH 16/16] update docstring --- src/pydata_sphinx_theme/short_link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pydata_sphinx_theme/short_link.py b/src/pydata_sphinx_theme/short_link.py index bfdcc155a1..f9f147f4c0 100644 --- a/src/pydata_sphinx_theme/short_link.py +++ b/src/pydata_sphinx_theme/short_link.py @@ -128,7 +128,7 @@ def shorten_gitlab(url: str) -> str: Convert a GitLab URL to a short form like group/project!123 or group/project@abcdef7. - Supports both canonical ('/-/') and non-canonical path formats. + Only supports canonical ('/-/') GitLab URLs. """ path = urlparse(url).path