Something went wrong. Try again.
a local-first atmospheric chalky task planner
Something went wrong. Try again.
2.0 kB · 55 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556// Split plain text into alternating text/link segments so a subtask can be// rendered with real <a> tags via Svelte templating (no `{@html}`, no XSS// surface — see markdown.ts for the hardened-HTML approach notes take).
export type LinkifyPart = { text: string } | { link: string };
// A run of non-whitespace starting at http(s):// — the trailing-punctuation// trim below decides how much of the run is really part of the URL.const URL_RE = /https?:\/\/[^\s<>"']+/g;
// Strip trailing punctuation that's almost certainly sentence punctuation// rather than part of the URL (e.g. "check https://x.com." or "see// (https://x.com)."). A trailing ')' is only stripped when it's unbalanced// within the match — "https://x.com/foo(bar)" keeps its ')'.function trimTrailingPunct(url: string): string { let end = url.length; while (end > 0) { const ch = url[end - 1]; if (ch === ")") { const slice = url.slice(0, end); const opens = (slice.match(/\(/g) ?? []).length; const closes = (slice.match(/\)/g) ?? []).length; if (closes > opens) { end--; continue; } break; } if (".,;:!?".includes(ch)) { end--; continue; } break; } return url.slice(0, end);}
/** Break `text` into segments, pulling out http(s) URLs as `{ link }` parts. */export function linkify(text: string): LinkifyPart[] { const parts: LinkifyPart[] = []; let lastIndex = 0; for (const match of text.matchAll(URL_RE)) { const raw = match[0]; const start = match.index ?? 0; const url = trimTrailingPunct(raw); if (!url) continue; if (start > lastIndex) parts.push({ text: text.slice(lastIndex, start) }); parts.push({ link: url }); const consumedEnd = start + url.length; if (consumedEnd < start + raw.length) parts.push({ text: raw.slice(url.length) }); lastIndex = start + raw.length; } if (lastIndex < text.length) parts.push({ text: text.slice(lastIndex) }); return parts;}