Compare commits

...

4 Commits

Author SHA1 Message Date
Ryan Di
0ef2611633 refactor: rename 2025-05-29 12:21:23 +10:00
Ryan Di
46c12a9f8c feat: add embeds to command palette 2025-05-29 12:00:01 +10:00
Ryan Di
20dae101e9 feat: support slido in embeddable 2025-05-29 11:51:33 +10:00
Ryan Di
022c407e24 feat: include google forms 2025-05-29 11:24:59 +10:00
3 changed files with 42 additions and 5 deletions

View File

@ -44,6 +44,8 @@ const RE_TWITTER_EMBED =
const RE_VALTOWN = const RE_VALTOWN =
/^https:\/\/(?:www\.)?val\.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/; /^https:\/\/(?:www\.)?val\.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
const RE_SLIDO = /^https:\/\/app\.sli\.do\/event\/([a-zA-Z0-9]+)/;
const RE_GENERIC_EMBED = const RE_GENERIC_EMBED =
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i; /^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
@ -56,7 +58,7 @@ const RE_REDDIT =
const RE_REDDIT_EMBED = const RE_REDDIT_EMBED =
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i; /^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
const ALLOWED_DOMAINS = new Set([ const ALLOWED_EMBED_HOSTS = new Set([
"youtube.com", "youtube.com",
"youtu.be", "youtu.be",
"vimeo.com", "vimeo.com",
@ -72,6 +74,9 @@ const ALLOWED_DOMAINS = new Set([
"giphy.com", "giphy.com",
"reddit.com", "reddit.com",
"forms.microsoft.com", "forms.microsoft.com",
"forms.gle",
"docs.google.com/forms",
"app.sli.do",
]); ]);
const ALLOW_SAME_ORIGIN = new Set([ const ALLOW_SAME_ORIGIN = new Set([
@ -86,6 +91,9 @@ const ALLOW_SAME_ORIGIN = new Set([
"stackblitz.com", "stackblitz.com",
"reddit.com", "reddit.com",
"forms.microsoft.com", "forms.microsoft.com",
"forms.gle",
"docs.google.com",
"app.sli.do",
]); ]);
export const createSrcDoc = (body: string) => { export const createSrcDoc = (body: string) => {
@ -278,6 +286,23 @@ export const getEmbedLink = (
return ret; return ret;
} }
if (RE_SLIDO.test(link)) {
const [, eventId] = link.match(RE_SLIDO)!;
link = `https://app.sli.do/event/${eventId}`;
embeddedLinkCache.set(originalLink, {
link,
intrinsicSize: aspectRatio,
type,
sandbox: { allowSameOrigin },
});
return {
link,
intrinsicSize: aspectRatio,
type,
sandbox: { allowSameOrigin },
};
}
embeddedLinkCache.set(link, { embeddedLinkCache.set(link, {
link, link,
intrinsicSize: aspectRatio, intrinsicSize: aspectRatio,
@ -335,20 +360,29 @@ const matchHostname = (
allowedHostnames: Set<string> | string, allowedHostnames: Set<string> | string,
): string | null => { ): string | null => {
try { try {
const { hostname } = new URL(url); const { hostname, pathname } = new URL(url);
const bareDomain = hostname.replace(/^www\./, ""); const bareDomain = hostname.replace(/^www\./, "");
if (allowedHostnames instanceof Set) { if (allowedHostnames instanceof Set) {
if (ALLOWED_DOMAINS.has(bareDomain)) { // Check for exact domain match
if (ALLOWED_EMBED_HOSTS.has(bareDomain)) {
return bareDomain; return bareDomain;
} }
// Check for path-based match (e.g., docs.google.com/forms)
const domainWithPath = `${bareDomain}${
pathname.split("/")[1] ? `/${pathname.split("/")[1]}` : ""
}`;
if (ALLOWED_EMBED_HOSTS.has(domainWithPath)) {
return domainWithPath;
}
const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace( const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace(
/^([^.]+)/, /^([^.]+)/,
"*", "*",
); );
if (ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded)) { if (ALLOWED_EMBED_HOSTS.has(bareDomainWithFirstSubdomainWildcarded)) {
return bareDomainWithFirstSubdomainWildcarded; return bareDomainWithFirstSubdomainWildcarded;
} }
return null; return null;
@ -399,6 +433,7 @@ export const embeddableURLValidator = (
if (!url) { if (!url) {
return false; return false;
} }
if (validateEmbeddable != null) { if (validateEmbeddable != null) {
if (typeof validateEmbeddable === "function") { if (typeof validateEmbeddable === "function") {
const ret = validateEmbeddable(url); const ret = validateEmbeddable(url);
@ -424,5 +459,5 @@ export const embeddableURLValidator = (
} }
} }
return !!matchHostname(url, ALLOWED_DOMAINS); return !!matchHostname(url, ALLOWED_EMBED_HOSTS);
}; };

View File

@ -11,6 +11,7 @@ export const actionSetEmbeddableAsActiveTool = register({
trackEvent: { category: "toolbar" }, trackEvent: { category: "toolbar" },
target: "Tool", target: "Tool",
label: "toolBar.embeddable", label: "toolBar.embeddable",
keywords: ["embeddable", "embed", "iframe"],
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const nextActiveTool = updateActiveTool(appState, { const nextActiveTool = updateActiveTool(appState, {
type: "embeddable", type: "embeddable",

View File

@ -319,6 +319,7 @@ function CommandPaletteInner({
actionManager.actions.toggleHandTool, actionManager.actions.toggleHandTool,
actionManager.actions.setFrameAsActiveTool, actionManager.actions.setFrameAsActiveTool,
actionManager.actions.toggleLassoTool, actionManager.actions.toggleLassoTool,
actionManager.actions.setEmbeddableAsActiveTool,
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools)); ].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools));
const editorCommands: CommandPaletteItem[] = [ const editorCommands: CommandPaletteItem[] = [