/**
* Embed providers (D-05). Stored posts hold only the placeholder
* ; the layer turns it
* into the real iframe. Adding a provider is one entry here and one in the
* layer's table.
*/
export interface EmbedProvider {
/** The name shown on the editor card. */
name: string
/** The id the placeholder may hold. */
idPattern: RegExp
/** Returns the id from a provider URL, or null. */
idFromUrl: (url: URL) => string | null
}
const YOUTUBE_HOSTS = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'music.youtube.com', 'youtube-nocookie.com', 'www.youtube-nocookie.com'])
export const EMBED_PROVIDERS = {
youtube: {
name: 'YouTube',
idPattern: /^[A-Za-z0-9_-]{11}$/,
idFromUrl(url: URL): string | null {
const host = url.hostname.toLowerCase()
if (host === 'youtu.be') {
return url.pathname.split('/')[1] ?? null
}
if (!YOUTUBE_HOSTS.has(host)) return null
if (url.pathname === '/watch') return url.searchParams.get('v')
const match = /^\/(?:shorts|embed)\/([^/]+)\/?$/.exec(url.pathname)
return match ? match[1]! : null
},
},
} as const satisfies Record
export type EmbedProviderName = keyof typeof EMBED_PROVIDERS
export function isEmbedProvider(name: string): name is EmbedProviderName {
return Object.prototype.hasOwnProperty.call(EMBED_PROVIDERS, name)
}
/**
* parseEmbedUrl returns the provider and id for a supported video URL
* (youtube.com/watch?v=, youtu.be/, youtube.com/shorts/ and
* youtube.com/embed/ for YouTube), or null for anything else.
*/
export function parseEmbedUrl(input: string): { provider: EmbedProviderName; id: string } | null {
let url: URL
try {
url = new URL(input.trim())
} catch {
return null
}
if (url.protocol !== 'https:' && url.protocol !== 'http:') return null
for (const provider of Object.keys(EMBED_PROVIDERS) as EmbedProviderName[]) {
const id = EMBED_PROVIDERS[provider].idFromUrl(url)
if (id && EMBED_PROVIDERS[provider].idPattern.test(id)) return { provider, id }
}
return null
}