From 216b69a0bc1defb96e2e0f00eaca7c0e9b0f9a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Mar 2026 19:18:58 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A8=20Handle=20external=20links=20`tar?= =?UTF-8?q?get=3D=5Fblank`=20and=20CSS=20automatically=20in=20JS=20and=20C?= =?UTF-8?q?SS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/css/custom.css | 76 ++++++++++++++++++++++++++++++++++++++------- docs/js/custom.js | 30 +++++++++++++++++- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/docs/css/custom.css b/docs/css/custom.css index 6ac827ff07..a1267e60bd 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -27,18 +27,70 @@ display: none; } -a.external-link::after { - /* \00A0 is a non-breaking space - to make the mark be on the same line as the link - */ - content: "\00A0[↪]"; -} - -a.internal-link::after { - /* \00A0 is a non-breaking space - to make the mark be on the same line as the link - */ - content: "\00A0↪"; +/* External links: detected by JS comparing origin to site origin + JS sets data-external-link on links pointing outside the site + Skip image links, .no-link-icon, and .announce-link */ +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link) { + /* For right to left languages */ + direction: ltr; + display: inline-block; +} + +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link)::after { + content: ""; + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-left: 0.25em; + vertical-align: middle; + opacity: 0.55; + background: currentColor; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cline x1='10' y1='14' x2='21' y2='3'/%3E%3C/svg%3E"); + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cline x1='10' y1='14' x2='21' y2='3'/%3E%3C/svg%3E"); + mask-size: contain; + mask-repeat: no-repeat; +} + +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link):hover::after { + opacity: 0.85; +} + +/* Internal links opening in new tab: same-origin links with target=_blank + JS sets data-internal-link on links pointing to the same site origin + Skip image links, .no-link-icon, and .announce-link */ +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link) { + /* For right to left languages */ + direction: ltr; + display: inline-block; +} + +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link)::after { + content: ""; + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-left: 0.25em; + vertical-align: middle; + opacity: 0.55; + background: currentColor; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='7' width='14' height='14' rx='2'/%3E%3Cpath d='M7 3h14v14'/%3E%3C/svg%3E"); + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='7' width='14' height='14' rx='2'/%3E%3Cpath d='M7 3h14v14'/%3E%3C/svg%3E"); + mask-size: contain; + mask-repeat: no-repeat; +} + +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link):hover::after { + opacity: 0.85; +} + +/* Disable link icons in footer and header nav */ +.md-footer a::after, +.md-header a::after { + content: none !important; } .shadow { diff --git a/docs/js/custom.js b/docs/js/custom.js index 82c1d45570..29f2bf8524 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -109,8 +109,36 @@ function setupTermynal() { loadVisibleTermynals(); } +function openLinksInNewTab() { + const siteUrl = document.querySelector("link[rel='canonical']")?.href + || window.location.origin; + const siteOrigin = new URL(siteUrl).origin; + document.querySelectorAll(".md-content a[href]").forEach(a => { + if (a.getAttribute("target") === "_self") return; + const href = a.getAttribute("href"); + if (!href) return; + try { + const url = new URL(href, window.location.href); + // Skip same-page anchor links (only the hash differs) + if (url.origin === window.location.origin + && url.pathname === window.location.pathname + && url.search === window.location.search) return; + if (!a.hasAttribute("target")) { + a.setAttribute("target", "_blank"); + a.setAttribute("rel", "noopener"); + } + if (url.origin !== siteOrigin) { + a.dataset.externalLink = ""; + } else { + a.dataset.internalLink = ""; + } + } catch (_) {} + }); +} + async function main() { - setupTermynal() + setupTermynal(); + openLinksInNewTab(); } document$.subscribe(() => {