From 2c1cc8ced1fd29d60370e51dbe1ca90e86465cd1 Mon Sep 17 00:00:00 2001 From: rktjmp Date: Sat, 7 Feb 2026 02:01:39 +1100 Subject: [PATCH] Make any frontmatter :// URI's clickable (#1822) * Make any frontmatter :// URI's clickable http/https links are opened in a new tab/window (via target="_blank"), other schemes are opened in the current tab, as the browser will prompt the user to open another application (or do this automatically). Per https://www.rfc-editor.org/rfc/rfc3986#section-3.1, the scheme must follow the pattern: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) * Make `mailto:...` strings clickable in frontmatter --- client/codemirror/frontmatter.ts | 48 +++++++++++++++++++++++++++-- client/markdown_parser/constants.ts | 3 +- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/client/codemirror/frontmatter.ts b/client/codemirror/frontmatter.ts index 8eaba51f..b35b1db0 100644 --- a/client/codemirror/frontmatter.ts +++ b/client/codemirror/frontmatter.ts @@ -12,6 +12,7 @@ import { frontmatterQuotesRegex, frontmatterUrlRegex, frontmatterWikiLinkRegex, + frontmatterMailtoRegex, } from "../markdown_parser/constants.ts"; import { processWikiLink, type WikiLinkMatch } from "./wiki_link_processor.ts"; @@ -78,7 +79,7 @@ export function frontmatterPlugin(client: Client) { const to = from + oMatch[0].length; const text = state.sliceDoc(from, to); - // 1) External links: http(s) URLs + // 1) External links: http(s), :// URLs frontmatterUrlRegex.lastIndex = 0; let match: RegExpExecArray | null; while ((match = frontmatterUrlRegex.exec(text)) !== null) { @@ -103,7 +104,14 @@ export function frontmatterPlugin(client: Client) { return; } try { - globalThis.open(url, "_blank"); + // Open http(s) links in a new window/tab, open + // alternate schemes in the same page, as they'll + // bounce to another application. + if (/^https?:\/\//i.test(url)) { + globalThis.open(url, "_blank"); + } else { + globalThis.open(url, "_self"); + } } catch (err) { console.error("Failed to open external link", err); } @@ -160,6 +168,42 @@ export function frontmatterPlugin(client: Client) { widgets.push(...decorations); } + + // 3) mailto:... links + frontmatterMailtoRegex.lastIndex = 0; + let mMatch: RegExpExecArray | null; + while ((mMatch = frontmatterMailtoRegex.exec(text)) !== null) { + const mFrom = from + (mMatch.index ?? 0); + const mTo = mFrom + mMatch[0].length; + const url = mMatch[1]; + const address = url.slice(7); + widgets.push( + Decoration.replace({ + widget: new LinkWidget({ + text: url, + title: `Mail ${address}`, + href: url, + cssClass: "sb-external-link", + from: mFrom, + callback: (e) => { + if (e.altKey) { + // Move cursor into the link + client.editorView.dispatch({ + selection: { anchor: mFrom }, + }); + client.focus(); + return; + } + try { + globalThis.open(url, "_self"); + } catch (err) { + console.error("Failed to open external link", err); + } + }, + }), + }).range(mFrom, mTo), + ); + } } } }, diff --git a/client/markdown_parser/constants.ts b/client/markdown_parser/constants.ts index ff64126d..250e70ae 100644 --- a/client/markdown_parser/constants.ts +++ b/client/markdown_parser/constants.ts @@ -6,7 +6,8 @@ export const tagRegex = export const nakedUrlRegex = /(^https?:\/\/([-a-zA-Z0-9@:%_\+~#=]|(?:[.](?!(\s|$)))){1,256})(([-a-zA-Z0-9(@:%_\+~#?&=\/]|(?:[.,:;)](?!(\s|$))))*)/; export const frontmatterQuotesRegex = /["'].*["']/g; -export const frontmatterUrlRegex = /(https?:\/\/[^\s"']+)/g; +export const frontmatterUrlRegex = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"']+)/g; export const frontmatterWikiLinkRegex = /(?!?\[\[)(?.*?)(?:\|(?.*?))?(?\]\])/g; +export const frontmatterMailtoRegex = /(mailto:[^@\s]+@[^@\s"']+)/ig; export const pWikiLinkRegex = new RegExp("^" + wikiLinkRegex.source); // Modified regex used only in parser