feat: linkify also catches www.* and bare domain.tld/path URLs
The description linkifier only caught http(s):// URLs; extend it to www.* hosts and bare domain.tld/path URLs (both get an https:// href prefix), so links like www.amelielens.com and facebook.com/amelielensmusic become clickable too. A required path + letters-only TLD keep false positives (version numbers, file names) out; bare @handles and #hashtags stay plain (the platform is ambiguous).
This commit is contained in:
@@ -1,13 +1,20 @@
|
||||
import { Fragment, type ReactNode } from "react";
|
||||
|
||||
// A bare http(s) URL: greedy up to whitespace/<, but don't swallow a trailing sentence
|
||||
// punctuation mark (so "…see https://x.com/y." links "https://x.com/y", not the dot).
|
||||
const URL_RE = /(https?:\/\/[^\s<]+[^\s<.,!?;:)\]}"'])/g;
|
||||
// Recognises three link shapes in plain text:
|
||||
// 1. explicit http(s):// URLs
|
||||
// 2. www.* bare hosts
|
||||
// 3. domain.tld/path bare URLs — the required path AND a letters-only TLD keep false positives
|
||||
// (version numbers like "3.2/x", file names like "index.html") from matching.
|
||||
// The last matched char excludes trailing sentence punctuation, so "see foo.com/bar." links
|
||||
// "foo.com/bar" and leaves the period. Bare @handles are intentionally NOT linked — the platform
|
||||
// is ambiguous.
|
||||
const URL_RE =
|
||||
/((?:https?:\/\/|www\.)[^\s<]+[^\s<.,!?;:)\]}"']|[a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,}\/[^\s<]*[^\s<.,!?;:)\]}"'])/gi;
|
||||
|
||||
/**
|
||||
* Turn bare http(s) URLs inside a plain-text string into clickable links that open in a new tab
|
||||
* (with a safe rel). Everything else is returned as text, so it composes with `whitespace-pre-wrap`
|
||||
* (line breaks preserved). Used for channel/video descriptions where links arrive as plain text.
|
||||
* Turn bare URLs (http(s), www, or domain.tld/path) inside a plain-text string into links that open
|
||||
* in a new tab (safe rel). Non-http matches get an https:// prefix on the href, but keep their
|
||||
* original text. Everything else is returned as text, so it composes with `whitespace-pre-wrap`.
|
||||
*/
|
||||
export function linkify(text: string): ReactNode[] {
|
||||
const out: ReactNode[] = [];
|
||||
@@ -17,19 +24,20 @@ export function linkify(text: string): ReactNode[] {
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = URL_RE.exec(text)) !== null) {
|
||||
if (m.index > last) out.push(<Fragment key={key++}>{text.slice(last, m.index)}</Fragment>);
|
||||
const url = m[0];
|
||||
const raw = m[0];
|
||||
const href = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
||||
out.push(
|
||||
<a
|
||||
key={key++}
|
||||
href={url}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:underline break-all"
|
||||
>
|
||||
{url}
|
||||
{raw}
|
||||
</a>,
|
||||
);
|
||||
last = m.index + url.length;
|
||||
last = m.index + raw.length;
|
||||
}
|
||||
if (last < text.length) out.push(<Fragment key={key++}>{text.slice(last)}</Fragment>);
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user