type AnyRecord = Record<string, any>
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)
}