//// Render embedded previews for social media links as Lustre HTML elements. //// //// Use `detect` to parse a URL into an `Embed`, `render` to turn it into //// HTML, or `embed` to do both in one step. All functions have `_with` //// variants that accept a custom `Config`. //// //// ## Example //// //// ```gleam //// case inlay.embed("https://www.youtube.com/watch?v=dQw4w9WgXcQ") { //// Some(element) -> element //// None -> html.text("Not an embeddable link") //// } //// ``` //// //// ## Configuration //// //// ```gleam //// let config = //// inlay.new() //// |> inlay.youtube(inlay.youtube_config()) //// |> inlay.mastodon(inlay.mastodon_config(["mastodon.social"])) //// ``` import gleam/option.{type Option, None, Some} import gleam/uri.{type Uri} import inlay/apple_music import inlay/bluesky import inlay/embed.{ AppleMusicMedia, BlueskyPost, Config, InstagramPost, MapLocation, MastodonPost, PixelfedPost, SoundCloudTrack, SpotifyMedia, TedTalk, TikTokVideo, Tweet, TwitchChannel, TwitchVideo, VimeoVideo, YoutubePlaylist, YoutubeVideo, } import inlay/instagram import inlay/mastodon import inlay/openstreetmap import inlay/pixelfed import inlay/soundcloud import inlay/spotify import inlay/ted import inlay/tiktok import inlay/twitch import inlay/twitter import inlay/vimeo import inlay/youtube import lustre/attribute import lustre/element.{type Element} import lustre/element/html /// A detected embeddable link with provider-specific data. /// Match on variants to inspect or handle providers individually. pub type Embed = embed.Embed pub type SpotifyMediaType = embed.SpotifyMediaType pub type InstagramPostType = embed.InstagramPostType /// Provider configuration. Create with `default_config()` or `new()`, /// then customize with builder functions like `youtube()`, `no_twitter()`. pub type Config = embed.Config pub type YoutubeConfig = embed.YoutubeConfig pub type VimeoConfig = embed.VimeoConfig pub type SpotifyConfig = embed.SpotifyConfig pub type BlueskyConfig = embed.BlueskyConfig pub type TwitchConfig = embed.TwitchConfig pub type OpenStreetMapConfig = embed.OpenStreetMapConfig pub type TedConfig = embed.TedConfig pub type SoundCloudConfig = embed.SoundCloudConfig pub type MastodonConfig = embed.MastodonConfig pub type PixelfedConfig = embed.PixelfedConfig pub type PixelfedLayout = embed.PixelfedLayout pub type AppleMusicConfig = embed.AppleMusicConfig pub type AppleMusicMediaType = embed.AppleMusicMediaType /// Create a default Bluesky configuration with no handle resolver. pub fn bluesky_config() -> BlueskyConfig { embed.bluesky_config() } /// Create a default YouTube configuration with privacy-enhanced mode enabled. pub fn youtube_config() -> YoutubeConfig { embed.youtube_config() } /// Create a default Vimeo configuration with Do Not Track enabled. pub fn vimeo_config() -> VimeoConfig { embed.vimeo_config() } /// Create a default Spotify configuration. pub fn spotify_config() -> SpotifyConfig { embed.spotify_config() } /// Create a Twitch configuration. The `parent` domain is required by /// Twitch's embed API. pub fn twitch_config(parent: String) -> TwitchConfig { embed.twitch_config(parent) } /// Create a default OpenStreetMap configuration. pub fn openstreetmap_config() -> OpenStreetMapConfig { embed.openstreetmap_config() } /// Create a default TED configuration. pub fn ted_config() -> TedConfig { embed.ted_config() } /// Create a default SoundCloud configuration. pub fn soundcloud_config() -> SoundCloudConfig { embed.soundcloud_config() } /// Create a Mastodon configuration. Only posts from the listed `servers` /// will be detected. pub fn mastodon_config(servers: List(String)) -> MastodonConfig { embed.mastodon_config(servers) } /// Create a Pixelfed configuration. Only posts from the listed `servers` /// will be detected. pub fn pixelfed_config( servers: List(String), layout: PixelfedLayout, ) -> PixelfedConfig { embed.pixelfed_config(servers, layout) } /// Pixelfed embed layout that shows the post with optional caption and likes. pub fn pixelfed_full( caption caption: Bool, likes likes: Bool, ) -> PixelfedLayout { embed.Full(caption: caption, likes: likes) } /// Pixelfed embed layout that shows a compact view of the post. pub fn pixelfed_compact() -> PixelfedLayout { embed.Compact } /// Create a default Apple Music configuration. pub fn apple_music_config() -> AppleMusicConfig { embed.apple_music_config() } /// Create a configuration with commonly used providers enabled. /// /// Enabled: YouTube, Vimeo, Spotify, Twitter/X, TikTok, Bluesky, /// Instagram, OpenStreetMap, TED, SoundCloud, Apple Music. /// /// Disabled (require explicit setup): Twitch, Mastodon, Pixelfed. pub fn default_config() -> Config { embed.default_config() } /// Create a configuration with all providers disabled. /// Enable providers selectively with the builder functions. pub fn new() -> Config { embed.new() } /// Set whether YouTube embeds use the privacy-enhanced `youtube-nocookie.com` /// domain. Enabled by default. pub fn youtube_no_cookie( config: YoutubeConfig, no_cookie: Bool, ) -> YoutubeConfig { embed.YoutubeConfig(..config, no_cookie: no_cookie) } /// Set whether Vimeo embeds include the Do Not Track flag. /// Enabled by default. pub fn vimeo_dnt(config: VimeoConfig, dnt: Bool) -> VimeoConfig { embed.VimeoConfig(..config, dnt: dnt) } /// Set a function that resolves a Bluesky handle (e.g. `"user.bsky.social"`) /// to a DID. When provided, embeds render with richer Bluesky markup. pub fn bluesky_resolver( config: BlueskyConfig, resolve: fn(String) -> Result(String, Nil), ) -> BlueskyConfig { let embed.BlueskyConfig(..) = config embed.BlueskyConfig(resolve_handle: Some(resolve)) } /// Enable YouTube embeds with the given configuration. pub fn youtube(config: Config, youtube_config: YoutubeConfig) -> Config { Config(..config, youtube: Some(youtube_config)) } /// Disable YouTube embeds. pub fn no_youtube(config: Config) -> Config { Config(..config, youtube: None) } /// Enable Vimeo embeds with the given configuration. pub fn vimeo(config: Config, vimeo_config: VimeoConfig) -> Config { Config(..config, vimeo: Some(vimeo_config)) } /// Disable Vimeo embeds. pub fn no_vimeo(config: Config) -> Config { Config(..config, vimeo: None) } /// Enable Spotify embeds with the given configuration. pub fn spotify(config: Config, spotify_config: SpotifyConfig) -> Config { Config(..config, spotify: Some(spotify_config)) } /// Disable Spotify embeds. pub fn no_spotify(config: Config) -> Config { Config(..config, spotify: None) } /// Enable Twitter/X embeds. pub fn twitter(config: Config) -> Config { Config(..config, twitter: Some(Nil)) } /// Disable Twitter/X embeds. pub fn no_twitter(config: Config) -> Config { Config(..config, twitter: None) } /// Enable TikTok embeds. pub fn tiktok(config: Config) -> Config { Config(..config, tiktok: Some(Nil)) } /// Disable TikTok embeds. pub fn no_tiktok(config: Config) -> Config { Config(..config, tiktok: None) } /// Enable Bluesky embeds with the given configuration. pub fn bluesky(config: Config, bluesky_config: BlueskyConfig) -> Config { Config(..config, bluesky: Some(bluesky_config)) } /// Disable Bluesky embeds. pub fn no_bluesky(config: Config) -> Config { Config(..config, bluesky: None) } /// Enable Instagram embeds. pub fn instagram(config: Config) -> Config { Config(..config, instagram: Some(Nil)) } /// Disable Instagram embeds. pub fn no_instagram(config: Config) -> Config { Config(..config, instagram: None) } /// Enable Twitch embeds with the given configuration. pub fn twitch(config: Config, twitch_config: TwitchConfig) -> Config { Config(..config, twitch: Some(twitch_config)) } /// Disable Twitch embeds. pub fn no_twitch(config: Config) -> Config { Config(..config, twitch: None) } /// Enable OpenStreetMap embeds with the given configuration. pub fn openstreetmap( config: Config, osm_config: OpenStreetMapConfig, ) -> Config { Config(..config, openstreetmap: Some(osm_config)) } /// Disable OpenStreetMap embeds. pub fn no_openstreetmap(config: Config) -> Config { Config(..config, openstreetmap: None) } /// Enable TED talk embeds with the given configuration. pub fn ted(config: Config, ted_config: TedConfig) -> Config { Config(..config, ted: Some(ted_config)) } /// Disable TED talk embeds. pub fn no_ted(config: Config) -> Config { Config(..config, ted: None) } /// Enable SoundCloud embeds with the given configuration. pub fn soundcloud( config: Config, soundcloud_config: SoundCloudConfig, ) -> Config { Config(..config, soundcloud: Some(soundcloud_config)) } /// Disable SoundCloud embeds. pub fn no_soundcloud(config: Config) -> Config { Config(..config, soundcloud: None) } /// Enable Mastodon embeds with the given configuration. pub fn mastodon(config: Config, mastodon_config: MastodonConfig) -> Config { Config(..config, mastodon: Some(mastodon_config)) } /// Disable Mastodon embeds. pub fn no_mastodon(config: Config) -> Config { Config(..config, mastodon: None) } /// Enable Pixelfed embeds with the given configuration. pub fn pixelfed(config: Config, pixelfed_config: PixelfedConfig) -> Config { Config(..config, pixelfed: Some(pixelfed_config)) } /// Disable Pixelfed embeds. pub fn no_pixelfed(config: Config) -> Config { Config(..config, pixelfed: None) } /// Enable Apple Music embeds with the given configuration. pub fn apple_music( config: Config, apple_music_config: AppleMusicConfig, ) -> Config { Config(..config, apple_music: Some(apple_music_config)) } /// Disable Apple Music embeds. pub fn no_apple_music(config: Config) -> Config { Config(..config, apple_music: None) } /// Detect an embeddable link from a URL using the default configuration. pub fn detect(url: String) -> Option(Embed) { detect_with(url, default_config()) } /// Detect an embeddable link from a URL using a custom configuration. pub fn detect_with(url: String, config: Config) -> Option(Embed) { case uri.parse(url) { Ok(parsed) -> do_detect(parsed, config) Error(_) -> None } } /// Render a detected embed as HTML using the default configuration. pub fn render(embed: Embed) -> Element(msg) { do_render(embed, default_config()) } /// Render a detected embed as HTML using a custom configuration. pub fn render_with(embed: Embed, config: Config) -> Element(msg) { do_render(embed, config) } /// Detect and render in one step using the default configuration. pub fn embed(url: String) -> Option(Element(msg)) { case detect(url) { Some(e) -> Some(render(e)) None -> None } } /// Detect and render in one step using a custom configuration. pub fn embed_with(url: String, config: Config) -> Option(Element(msg)) { case detect_with(url, config) { Some(e) -> Some(render_with(e, config)) None -> None } } /// Create an anchor component that renders embeds for recognized URLs /// and delegates to `fallback` for unrecognized ones. /// Uses the default configuration. pub fn a_component( fallback: fn(String, Option(String), List(Element(msg))) -> Element(msg), ) -> fn(String, Option(String), List(Element(msg))) -> Element(msg) { a_component_with(default_config(), fallback) } /// Create an anchor component with a custom configuration. pub fn a_component_with( config: Config, fallback: fn(String, Option(String), List(Element(msg))) -> Element(msg), ) -> fn(String, Option(String), List(Element(msg))) -> Element(msg) { fn(href: String, title: Option(String), children: List(Element(msg))) { case embed_with(href, config) { Some(el) -> el None -> fallback(href, title, children) } } } /// Create an anchor component using the default configuration and /// a standard `` tag fallback. pub fn a_component_default() -> fn(String, Option(String), List(Element(msg))) -> Element(msg) { a_component(default_fallback) } fn default_fallback( href: String, title: Option(String), children: List(Element(msg)), ) -> Element(msg) { let attrs = [attribute.href(href)] let attrs = case title { Some(t) -> [attribute.title(t), ..attrs] None -> attrs } html.a(attrs, children) } fn do_detect(url: Uri, config: Config) -> Option(Embed) { use <- try_one_with(config.mastodon, url, mastodon.detect) use <- try_one_with(config.pixelfed, url, pixelfed.detect) use <- try_one(config.youtube, url, youtube.detect) use <- try_one(config.ted, url, ted.detect) use <- try_one(config.vimeo, url, vimeo.detect) use <- try_one(config.spotify, url, spotify.detect) use <- try_one(config.bluesky, url, bluesky.detect) use <- try_one(config.twitch, url, twitch.detect) use <- try_one(config.soundcloud, url, soundcloud.detect) use <- try_one(config.twitter, url, twitter.detect) use <- try_one(config.tiktok, url, tiktok.detect) use <- try_one(config.instagram, url, instagram.detect) use <- try_one(config.openstreetmap, url, openstreetmap.detect) use <- try_one(config.apple_music, url, apple_music.detect) None } fn do_render(embed: Embed, config: Config) -> Element(msg) { let assert Ok(el) = case embed { YoutubeVideo(..) | YoutubePlaylist(..) -> youtube.render(embed, config) VimeoVideo(..) -> vimeo.render(embed, config) SpotifyMedia(..) -> spotify.render(embed, config) Tweet(..) -> twitter.render(embed, config) TikTokVideo(..) -> tiktok.render(embed, config) BlueskyPost(..) -> bluesky.render(embed, config) InstagramPost(..) -> instagram.render(embed, config) TwitchChannel(..) | TwitchVideo(..) -> twitch.render(embed, config) MapLocation(..) -> openstreetmap.render(embed, config) TedTalk(..) -> ted.render(embed, config) SoundCloudTrack(..) -> soundcloud.render(embed, config) MastodonPost(..) -> mastodon.render(embed, config) PixelfedPost(..) -> pixelfed.render(embed, config) AppleMusicMedia(..) -> apple_music.render(embed, config) } el } fn try_one( enabled: Option(a), url: Uri, detector: fn(Uri) -> Option(Embed), next: fn() -> Option(Embed), ) -> Option(Embed) { case enabled { Some(_) -> case detector(url) { Some(found) -> Some(found) None -> next() } None -> next() } } fn try_one_with( enabled: Option(a), url: Uri, detector: fn(Uri, a) -> Option(Embed), next: fn() -> Option(Embed), ) -> Option(Embed) { case enabled { Some(cfg) -> case detector(url, cfg) { Some(found) -> Some(found) None -> next() } None -> next() } }