(function () { // ===== util ===== function el(tag, attrs = {}, style = {}) { const e = document.createElement(tag); Object.entries(attrs).forEach(([k, v]) => { if (v === false || v == null) return; if (k in e) e[k] = v; else e.setAttribute(k, String(v)); }); Object.assign(e.style, style); return e; } function makeResponsive(wrapperChild, aspect = "16:9") { const [w, h] = aspect.split(":").map(Number); const pad = (h / w) * 100; const wrap = el("div", {}, { position: "relative", paddingBottom: pad + "%", height: "0", overflow: "hidden", maxWidth: "100%", }); Object.assign(wrapperChild.style, { position: "absolute", top: "0", left: "0", width: "100%", height: "100%", }); wrap.appendChild(wrapperChild); return wrap; } function getHostname(u) { return (u.hostname || "").replace(/^www\./, "").toLowerCase(); } function safeURL(raw) { try { return new URL(raw); } catch { return null; } } // ===== platform parsers ===== function parseYouTube(u) { const host = getHostname(u); let id = null; if (host === "youtube.com" || host === "m.youtube.com") { if (u.pathname === "/watch") id = u.searchParams.get("v"); else if (u.pathname.startsWith("/shorts/")) id = u.pathname.split("/")[2]; else if (u.pathname.startsWith("/embed/")) id = u.pathname.split("/")[2]; } else if (host === "youtu.be") { id = u.pathname.split("/")[1]; } if (!id) return null; const list = u.searchParams.get("list"); const t = u.searchParams.get("t") || u.searchParams.get("start"); // seconds or 1m30s (we'll best-effort) let embed = `https://www.youtube.com/embed/${encodeURIComponent(id)}`; const params = new URLSearchParams(); if (list) params.set("list", list); if (t) params.set("start", normalizeYouTubeTime(t)); // best-effort const qs = params.toString(); if (qs) embed += `?${qs}`; return { type: "iframe", src: embed, aspect: "16:9" }; } function normalizeYouTubeTime(t) { // supports: "90", "1m30s", "2h3m10s" if (/^\d+$/.test(t)) return t; let sec = 0; const m = t.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/i); if (m) { const h = parseInt(m[1] || "0", 10); const mi = parseInt(m[2] || "0", 10); const s = parseInt(m[3] || "0", 10); sec = h * 3600 + mi * 60 + s; } return String(sec || 0); } function parseVimeo(u) { const host = getHostname(u); if (host !== "vimeo.com" && host !== "player.vimeo.com") return null; // vimeo.com/123456789 or player.vimeo.com/video/123456789 const m = u.pathname.match(/\/(?:video\/)?(\d+)/); if (!m) return null; const id = m[1]; const embed = `https://player.vimeo.com/video/${encodeURIComponent(id)}`; return { type: "iframe", src: embed, aspect: "16:9" }; } function parseDailymotion(u) { const host = getHostname(u); if (host !== "dailymotion.com" && host !== "dai.ly") return null; let id = null; if (host === "dai.ly") { id = u.pathname.split("/")[1]; } else { // /video/{id} const m = u.pathname.match(/\/video\/([^_\/]+)/); if (m) id = m[1]; } if (!id) return null; const embed = `https://www.dailymotion.com/embed/video/${encodeURIComponent(id)}`; return { type: "iframe", src: embed, aspect: "16:9" }; } function parseTwitch(u) { const host = getHostname(u); if (host !== "twitch.tv" && host !== "www.twitch.tv" && host !== "clips.twitch.tv") return null; // Twitch embed requires parent=yourdomain.com // We'll auto-fill from current location hostname (works on same domain). const parent = window.location.hostname; // video: twitch.tv/videos/123 const videoMatch = u.pathname.match(/\/videos\/(\d+)/); if (videoMatch) { const id = videoMatch[1]; const embed = `https://player.twitch.tv/?video=${encodeURIComponent(id)}&parent=${encodeURIComponent(parent)}`; return { type: "iframe", src: embed, aspect: "16:9" }; } // clip: clips.twitch.tv/Slug or twitch.tv/{channel}/clip/Slug let slug = null; if (getHostname(u) === "clips.twitch.tv") { slug = u.pathname.split("/")[1]; } else { const clipMatch = u.pathname.match(/\/clip\/([^\/]+)/); if (clipMatch) slug = clipMatch[1]; } if (slug) { const embed = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(slug)}&parent=${encodeURIComponent(parent)}`; return { type: "iframe", src: embed, aspect: "16:9" }; } return null; } function parseLoom(u) { const host = getHostname(u); if (host !== "loom.com") return null; // loom.com/share/{id} const m = u.pathname.match(/\/share\/([a-zA-Z0-9]+)/); if (!m) return null; const id = m[1]; const embed = `https://www.loom.com/embed/${encodeURIComponent(id)}`; return { type: "iframe", src: embed, aspect: "16:9" }; } function parseSoundCloud(u) { const host = getHostname(u); if (host !== "soundcloud.com") return null; // SoundCloud official embed uses /player/?url= const embed = `https://w.soundcloud.com/player/?url=${encodeURIComponent(u.toString())}`; return { type: "iframe", src: embed, aspect: "16:9" }; } function parseSpotify(u) { const host = getHostname(u); if (host !== "open.spotify.com") return null; // spotify embed: https://open.spotify.com/embed/{type}/{id} const parts = u.pathname.split("/").filter(Boolean); // [type, id] if (parts.length < 2) return null; const type = parts[0]; const id = parts[1]; const embed = `https://open.spotify.com/embed/${encodeURIComponent(type)}/${encodeURIComponent(id)}`; // Spotify players are more square-ish, but 16:9 is ok; use 1:1-ish for better look return { type: "iframe", src: embed, aspect: "1:1" }; } const parsers = [ parseYouTube, parseVimeo, parseDailymotion, parseTwitch, parseLoom, parseSoundCloud, parseSpotify, ]; function buildEmbed(rawUrl) { const u = safeURL(rawUrl); if (!u) return null; for (const p of parsers) { const r = p(u); if (r) return r; } return null; } function replaceOneOembed(oembed) { const url = oembed.getAttribute("url") || oembed.getAttribute("data-url"); if (!url) return; const embed = buildEmbed(url); if (!embed) return; const iframe = el("iframe", { src: embed.src, title: "Embedded media", frameBorder: "0", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullscreen: true, loading: "lazy", referrerPolicy: "strict-origin-when-cross-origin", }); const node = makeResponsive(iframe, embed.aspect || "16:9"); const figure = oembed.closest("figure") || oembed.parentElement; if (figure) { figure.innerHTML = ""; figure.appendChild(node); } else { oembed.replaceWith(node); } } function run(root = document) { const oembeds = root.querySelectorAll("figure.media oembed, oembed[url], oembed[data-url]"); oembeds.forEach(replaceOneOembed); } // initial document.addEventListener("DOMContentLoaded", () => run(document)); // optional: if your editor injects content dynamically, this will catch it. const mo = new MutationObserver((mutations) => { for (const m of mutations) { m.addedNodes.forEach((n) => { if (!(n instanceof Element)) return; if (n.matches && (n.matches("oembed") || n.querySelector("oembed"))) run(n); }); } }); mo.observe(document.documentElement, { childList: true, subtree: true }); })();