const URL_PATTERN
= /\b((?:https?:\/\/|www\.)[^\s<]+|(?:[a-z0-9-]+\.)+[a-z]{2,24}\/[^\s<]*)/gi
const TRAILING_PUNCTUATION = /[)\].,;:!?'"]+$/
function balanceTrailing (url: string): { url: string, trailing: string } {
let trailing = ''
let current = url
while (true) {
const match = current.match(TRAILING_PUNCTUATION)
if (!match) break
const candidate = current.slice(0, -match[0].length)
const opens = (candidate.match(/\(/g) || []).length
const closes = (candidate.match(/\)/g) || []).length
if (match[0].includes(')') && opens > closes) break
trailing = match[0] + trailing
current = candidate
}
return { url: current, trailing }
}
function toHref (raw: string): string {
if (/^https?:\/\//i.test(raw)) return raw
return `https://${raw}`
}
/**
* Wrap bare URLs and domain-like tokens in Mastodon-style HTML with `` tags,
* leaving any text already inside an `` element untouched.
*
* Mastodon only auto-links text that starts with a protocol; references like
* `roe.dev/ama` stay as plain text in the rendered `content` field.
*/
export function linkifyBareUrls (html: string): string {
let result = ''
let cursor = 0
const anchor = /]*>[\s\S]*?<\/a>/gi
let match: RegExpExecArray | null
while ((match = anchor.exec(html))) {
result += linkifyText(html.slice(cursor, match.index))
result += match[0]
cursor = match.index + match[0].length
}
result += linkifyText(html.slice(cursor))
return result
}
function linkifyText (segment: string): string {
if (!segment) return segment
return segment.replace(URL_PATTERN, raw => {
const { url, trailing } = balanceTrailing(raw)
return `${url}${trailing}`
})
}