"""FlowDeck — Media embeds (v5.5.0): provider detection + iframe rewriting. Maps a raw http(s) URL to a provider-specific embed URL so that one generic ``embed`` block can render YouTube, Vimeo, Figma, Google Maps, Google Docs/Sheets/Slides, Loom, CodePen, Miro, Spotify, SoundCloud, Twitch, X/Twitter, Pinterest, Microsoft Office docs… exactly like Notion's universal embed. Unknown/showable URLs (PDF, images, direct video/audio files, plain http) fall back to a plain iframe so the link is still visible inline. """ from __future__ import annotations import re from urllib.parse import parse_qs, quote, urlparse def _q(params, key): vals = params.get(key) return vals[0] if vals else "" def _host_matches(netloc: str, host: str) -> bool: """True when ``netloc`` is ``host`` or one of its subdomains.""" netloc = (netloc or "").lower().split(":")[0] host = host.lower() return netloc == host or netloc.endswith("." + host) def _embed_youtube(url: str, path: str, params, ctx: dict) -> str | None: m = re.search(r"/(?:v|shorts|embed|live)/([A-Za-z0-9_-]{6,20})", path) vid = m.group(1) if m else _q(params, "v") if not vid: # youtu.be/ (short link) — the id is the first path segment. seg = path.strip("/").split("/")[0] if re.fullmatch(r"[A-Za-z0-9_-]{6,20}", seg or ""): vid = seg if not vid: return None start = _q(params, "t") or _q(params, "start") frag = f"?start={start}" if start else "" return f"https://www.youtube.com/embed/{vid}{frag}" def _embed_vimeo(url: str, path: str, params, ctx: dict) -> str | None: m = re.search(r"/(\d{6,12})", path) if not m: return None return f"https://player.vimeo.com/video/{m.group(1)}" def _embed_loom(url: str, path: str, params, ctx: dict) -> str | None: m = re.search(r"/(?:embed/|share/)?([0-9a-f]{32})", path) if not m: return None return f"https://www.loom.com/embed/{m.group(1)}" def _embed_figma(url: str, path: str, params, ctx: dict) -> str | None: clean = url.split("?", 1)[0] if "figma.com/file/" not in clean and "figma.com/proto/" not in clean and "figma.com/design/" not in clean: return None return "https://www.figma.com/embed?embed_host=flowdeck&url=" + quote(clean, safe="") def _embed_map(url: str, path: str, params, ctx: dict) -> str | None: if "google.com/maps" not in url and "maps.app.goo.gl" not in url: return None return "https://maps.google.com/maps?q=" + quote(url, safe="") + "&output=embed" def _embed_gdocs(url: str, path: str, params, ctx: dict) -> str | None: m = re.search( r"docs\.google\.com/(document|spreadsheets|presentation|forms)/d/([A-Za-z0-9_-]+)", url ) if not m: return None kind, doc_id = m.group(1), m.group(2) if kind == "forms": return f"https://docs.google.com/forms/d/{doc_id}/viewform?embedded=true" return f"https://docs.google.com/{kind}/d/{doc_id}/preview" def _embed_codepen(url: str, path: str, params, ctx: dict) -> str | None: m = re.search(r"codepen\.io/([^/]+)/pen/([^/?#]+)", url) if not m: return None return f"https://codepen.io/{m.group(1)}/embed/{m.group(2)}?default-tab=result" def _embed_miro(url: str, path: str, params, ctx: dict) -> str | None: m = re.search(r"miro\.com/app/(?:board|live-embed)/([^/?#]+)", url) if not m: return None return f"https://miro.com/app/live-embed/{m.group(1)}" def _embed_spotify(url: str, path: str, params, ctx: dict) -> str | None: m = re.search(r"/(track|playlist|album|episode|show|artist)/([A-Za-z0-9]+)", url) if not m: return None return f"https://open.spotify.com/embed/{m.group(1)}/{m.group(2)}" def _embed_soundcloud(url: str, path: str, params, ctx: dict) -> str | None: if "soundcloud.com" not in url: return None return "https://w.soundcloud.com/player/?url=" + quote(url, safe="") + "&color=%2300aaff" def _embed_twitch(url: str, path: str, params, ctx: dict) -> str | None: if "twitch.tv" not in url: return None parent = (ctx.get("parent") or "localhost").replace("https://", "").replace("http://", "").split("/")[0] video = re.search(r"twitch\.tv/videos/(\d+)", url) if video: return f"https://player.twitch.tv/?video={video.group(1)}&parent={parent}" m = re.search(r"twitch\.tv/([^/?#]+)", url) if not m or m.group(1) in ("videos", "directory"): return None return f"https://player.twitch.tv/?channel={m.group(1)}&parent={parent}" def _embed_twitter(url: str, path: str, params, ctx: dict) -> str | None: if "twitter.com" not in url and "x.com" not in url: return None m = re.search(r"/status(?:es)?/(\d+)", url) if not m: return f"https://platform.twitter.com/embed/Tweet.html?url={quote(url, safe='')}" return f"https://platform.twitter.com/embed/Tweet.html?id={m.group(1)}" def _embed_pinterest(url: str, path: str, params, ctx: dict) -> str | None: if "pinterest" not in url: return None return f"https://pinterest.com/pin/embed?url={quote(url, safe='')}" def _embed_office(url: str, path: str, params, ctx: dict) -> str | None: low = url.lower().split("?", 1)[0] if low.endswith((".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp")): return "https://view.officeapps.live.com/op/embed.aspx?src=" + quote(url, safe="") if "officeapps.live.com" in low or "sharepoint.com" in low or "1drv.ms" in low: return "https://view.officeapps.live.com/op/embed.aspx?src=" + quote(url, safe="") return None def _embed_files(url: str, path: str, params, ctx: dict) -> str | None: """Direct media: PDF/images/videos/audio can live in a plain iframe.""" return url # (host, handler) — order matters: more specific hosts first. _HANDLERS = ( ("youtube.com", _embed_youtube), ("youtu.be", _embed_youtube), ("vimeo.com", _embed_vimeo), ("loom.com", _embed_loom), ("figma.com", _embed_figma), ("docs.google.com", _embed_gdocs), ("google.com/maps", _embed_map), ("maps.app.goo.gl", _embed_map), ("codepen.io", _embed_codepen), ("miro.com", _embed_miro), ("open.spotify.com", _embed_spotify), ("spotify.com", _embed_spotify), ("soundcloud.com", _embed_soundcloud), ("twitch.tv", _embed_twitch), ("twitter.com", _embed_twitter), ("x.com", _embed_twitter), ("pinterest.", _embed_pinterest), ("office.com", _embed_office), ("officeapps.live.com", _embed_office), ("sharepoint.com", _embed_office), ("1drv.ms", _embed_office), ) _SCHEME_RE = re.compile(r"^([a-zA-Z][a-zA-Z0-9+.-]*):") def _parse(url: str): raw = url.strip() if not raw: return None, None, None m = _SCHEME_RE.match(raw) if m: if m.group(1).lower() not in ("http", "https"): return None, None, None # mailto:, tel:, javascript:, data:… else: raw = "https://" + raw u = urlparse(raw) if u.scheme not in ("http", "https") or not u.netloc: return None, None, None host = u.hostname or "" if "." not in host and host != "localhost": return None, None, None # a bare word is not a URL return raw, u, parse_qs(u.query) def embed_src(url: str, *, parent: str = "") -> str | None: """Return the embeddable iframe src for a URL, or None if it can't embed.""" raw, u, params = _parse(url) if raw is None: return None ctx = {"parent": parent} netloc = (u.netloc or "").lower() for needle, handler in _HANDLERS: if "/" in needle or needle.endswith("."): if needle in raw.lower(): return handler(raw, u.path, params, ctx) elif _host_matches(netloc, needle): return handler(raw, u.path, params, ctx) # Office documents hosted on arbitrary domains. office = _embed_office(raw, u.path, params, ctx) if office: return office return _embed_files(raw, u.path, params, ctx) _IMAGE_EXT = re.compile(r"\.(png|jpe?g|gif|webp|svg|bmp|ico|avif)$", re.I) _PDF_EXT = re.compile(r"\.pdf$", re.I) _VIDEO_EXT = re.compile(r"\.(mp4|webm|ogg|ogv|mov|m4v)$", re.I) _AUDIO_EXT = re.compile(r"\.(mp3|wav|ogg|oga|m4a|flac|aac)$", re.I) def inline_kind(url: str) -> str | None: """Best inline renderer for a URL: 'iframe' | 'image' | 'pdf' | 'video' | 'audio'. Returns None when the URL should open in a new tab.""" raw, u, _params = _parse(url) if raw is None: return None path = u.path or "" if _IMAGE_EXT.search(path): return "image" if _PDF_EXT.search(path): return "pdf" if _VIDEO_EXT.search(path): return "video" if _AUDIO_EXT.search(path): return "audio" return "iframe" def provider(url: str) -> str: """Human-readable provider name for a URL (used by the editor).""" raw, u, _params = _parse(url) if raw is None: return "" netloc = (u.netloc or "").lower() for needle, _handler in _HANDLERS: if "/" in needle or needle.endswith("."): if needle in raw.lower(): return needle.split(".")[0].rstrip(".") elif _host_matches(netloc, needle): name = needle.split(".")[0] return "youtube" if name == "youtu" else name return "" def resolve_embed(url: str, *, parent: str = "") -> dict: """Resolve a URL to ``{src, kind, provider}`` for the generic embed block.""" kind = inline_kind(url) return { "src": embed_src(url, parent=parent) or "", "kind": kind or "", "provider": provider(url), } def embed_html(src: str, *, height: int = 520) -> str: """A responsive, borderless iframe for a provider embed URL.""" return ( f'' )