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";
|
import { Fragment, type ReactNode } from "react";
|
||||||
|
|
||||||
// A bare http(s) URL: greedy up to whitespace/<, but don't swallow a trailing sentence
|
// Recognises three link shapes in plain text:
|
||||||
// punctuation mark (so "…see https://x.com/y." links "https://x.com/y", not the dot).
|
// 1. explicit http(s):// URLs
|
||||||
const URL_RE = /(https?:\/\/[^\s<]+[^\s<.,!?;:)\]}"'])/g;
|
// 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
|
* Turn bare URLs (http(s), www, or domain.tld/path) inside a plain-text string into links that open
|
||||||
* (with a safe rel). Everything else is returned as text, so it composes with `whitespace-pre-wrap`
|
* in a new tab (safe rel). Non-http matches get an https:// prefix on the href, but keep their
|
||||||
* (line breaks preserved). Used for channel/video descriptions where links arrive as plain text.
|
* original text. Everything else is returned as text, so it composes with `whitespace-pre-wrap`.
|
||||||
*/
|
*/
|
||||||
export function linkify(text: string): ReactNode[] {
|
export function linkify(text: string): ReactNode[] {
|
||||||
const out: ReactNode[] = [];
|
const out: ReactNode[] = [];
|
||||||
@@ -17,19 +24,20 @@ export function linkify(text: string): ReactNode[] {
|
|||||||
let m: RegExpExecArray | null;
|
let m: RegExpExecArray | null;
|
||||||
while ((m = URL_RE.exec(text)) !== null) {
|
while ((m = URL_RE.exec(text)) !== null) {
|
||||||
if (m.index > last) out.push(<Fragment key={key++}>{text.slice(last, m.index)}</Fragment>);
|
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(
|
out.push(
|
||||||
<a
|
<a
|
||||||
key={key++}
|
key={key++}
|
||||||
href={url}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-accent hover:underline break-all"
|
className="text-accent hover:underline break-all"
|
||||||
>
|
>
|
||||||
{url}
|
{raw}
|
||||||
</a>,
|
</a>,
|
||||||
);
|
);
|
||||||
last = m.index + url.length;
|
last = m.index + raw.length;
|
||||||
}
|
}
|
||||||
if (last < text.length) out.push(<Fragment key={key++}>{text.slice(last)}</Fragment>);
|
if (last < text.length) out.push(<Fragment key={key++}>{text.slice(last)}</Fragment>);
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
Reference in New Issue
Block a user