type AnyRecord = Record function normalizeHttpUrl(value: unknown, label: string) { let url: URL try { url = new URL(String(value || '')) } catch { throw new Error(`${label} must be a valid URL`) } if (url.protocol !== 'http:' && url.protocol !== 'https:') { throw new Error(`${label} must use http or https`) } return url.toString() } function trimTrailingUrlPunctuation(value: string) { return value.replace(/[.,!?;:)\]}]+$/u, '') } export function buildLinkFacets(text: string) { const facets: AnyRecord[] = [] const encoder = new TextEncoder() const matches = text.matchAll(/https?:\/\/[^\s]+/gu) for (const match of matches) { if (match.index === undefined) continue const matchedUrl = trimTrailingUrlPunctuation(match[0]) if (!matchedUrl) continue let uri: string try { uri = normalizeHttpUrl(matchedUrl, 'Post link') } catch { continue } const byteStart = encoder.encode(text.slice(0, match.index)).byteLength const byteEnd = byteStart + encoder.encode(matchedUrl).byteLength facets.push({ index: { byteStart, byteEnd }, features: [{ $type: 'app.bsky.richtext.facet#link', uri }], }) } return facets } export function normalizeExternalHttpUrl(value: unknown, label: string) { return normalizeHttpUrl(value, label) }