|  |  |  | @@ -12,11 +12,13 @@ import { | 
		
	
		
			
				|  |  |  |  |   NonDeletedExcalidrawElement, | 
		
	
		
			
				|  |  |  |  |   Theme, | 
		
	
		
			
				|  |  |  |  | } from "./types"; | 
		
	
		
			
				|  |  |  |  | import { sanitizeHTMLAttribute } from "../data/url"; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | type EmbeddedLink = | 
		
	
		
			
				|  |  |  |  |   | ({ | 
		
	
		
			
				|  |  |  |  |       aspectRatio: { w: number; h: number }; | 
		
	
		
			
				|  |  |  |  |       warning?: string; | 
		
	
		
			
				|  |  |  |  |       sandbox?: { allowSameOrigin?: boolean }; | 
		
	
		
			
				|  |  |  |  |     } & ( | 
		
	
		
			
				|  |  |  |  |       | { type: "video" | "generic"; link: string } | 
		
	
		
			
				|  |  |  |  |       | { type: "document"; srcdoc: (theme: Theme) => string } | 
		
	
	
		
			
				
					
					|  |  |  | @@ -28,20 +30,21 @@ const embeddedLinkCache = new Map<string, EmbeddedLink>(); | 
		
	
		
			
				|  |  |  |  | const RE_YOUTUBE = | 
		
	
		
			
				|  |  |  |  |   /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/; | 
		
	
		
			
				|  |  |  |  | const RE_VIMEO = | 
		
	
		
			
				|  |  |  |  |   /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/; | 
		
	
		
			
				|  |  |  |  |   /^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/; | 
		
	
		
			
				|  |  |  |  | const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | const RE_GH_GIST = /^https:\/\/gist\.github\.com/; | 
		
	
		
			
				|  |  |  |  | const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/; | 
		
	
		
			
				|  |  |  |  | const RE_GH_GIST_EMBED = | 
		
	
		
			
				|  |  |  |  |   /^<script[\s\S]*?\ssrc=["'](https:\/\/gist.github.com\/.*?)\.js["']/i; | 
		
	
		
			
				|  |  |  |  |   /^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | // not anchored to start to allow <blockquote> twitter embeds | 
		
	
		
			
				|  |  |  |  | const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/; | 
		
	
		
			
				|  |  |  |  | const RE_TWITTER = | 
		
	
		
			
				|  |  |  |  |   /(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/; | 
		
	
		
			
				|  |  |  |  | const RE_TWITTER_EMBED = | 
		
	
		
			
				|  |  |  |  |   /^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i; | 
		
	
		
			
				|  |  |  |  |   /^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:twitter|x)\.com\/[^"']*)/i; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | 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_GENERIC_EMBED = | 
		
	
		
			
				|  |  |  |  |   /^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i; | 
		
	
	
		
			
				
					
					|  |  |  | @@ -136,47 +139,38 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |   if (RE_TWITTER.test(link)) { | 
		
	
		
			
				|  |  |  |  |     let ret: EmbeddedLink; | 
		
	
		
			
				|  |  |  |  |     // assume embed code | 
		
	
		
			
				|  |  |  |  |     if (/<blockquote/.test(link)) { | 
		
	
		
			
				|  |  |  |  |       const srcDoc = createSrcDoc(link); | 
		
	
		
			
				|  |  |  |  |       ret = { | 
		
	
		
			
				|  |  |  |  |         type: "document", | 
		
	
		
			
				|  |  |  |  |         srcdoc: () => srcDoc, | 
		
	
		
			
				|  |  |  |  |         aspectRatio: { w: 480, h: 480 }, | 
		
	
		
			
				|  |  |  |  |       }; | 
		
	
		
			
				|  |  |  |  |       // assume regular tweet url | 
		
	
		
			
				|  |  |  |  |     } else { | 
		
	
		
			
				|  |  |  |  |       ret = { | 
		
	
		
			
				|  |  |  |  |     const postId = link.match(RE_TWITTER)![1]; | 
		
	
		
			
				|  |  |  |  |     // the embed srcdoc still supports twitter.com domain only. | 
		
	
		
			
				|  |  |  |  |     // Note that we don't attempt to parse the username as it can consist of | 
		
	
		
			
				|  |  |  |  |     // non-latin1 characters, and the username in the url can be set to anything | 
		
	
		
			
				|  |  |  |  |     // without affecting the embed. | 
		
	
		
			
				|  |  |  |  |     const safeURL = sanitizeHTMLAttribute( | 
		
	
		
			
				|  |  |  |  |       `https://twitter.com/x/status/${postId}`, | 
		
	
		
			
				|  |  |  |  |     ); | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     const ret: EmbeddedLink = { | 
		
	
		
			
				|  |  |  |  |       type: "document", | 
		
	
		
			
				|  |  |  |  |       srcdoc: (theme: string) => | 
		
	
		
			
				|  |  |  |  |         createSrcDoc( | 
		
	
		
			
				|  |  |  |  |             `<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`, | 
		
	
		
			
				|  |  |  |  |           `<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${safeURL}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`, | 
		
	
		
			
				|  |  |  |  |         ), | 
		
	
		
			
				|  |  |  |  |       aspectRatio: { w: 480, h: 480 }, | 
		
	
		
			
				|  |  |  |  |       sandbox: { allowSameOrigin: true }, | 
		
	
		
			
				|  |  |  |  |     }; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |     embeddedLinkCache.set(originalLink, ret); | 
		
	
		
			
				|  |  |  |  |     return ret; | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |   if (RE_GH_GIST.test(link)) { | 
		
	
		
			
				|  |  |  |  |     let ret: EmbeddedLink; | 
		
	
		
			
				|  |  |  |  |     // assume embed code | 
		
	
		
			
				|  |  |  |  |     if (/<script>/.test(link)) { | 
		
	
		
			
				|  |  |  |  |       const srcDoc = createSrcDoc(link); | 
		
	
		
			
				|  |  |  |  |       ret = { | 
		
	
		
			
				|  |  |  |  |         type: "document", | 
		
	
		
			
				|  |  |  |  |         srcdoc: () => srcDoc, | 
		
	
		
			
				|  |  |  |  |         aspectRatio: { w: 550, h: 720 }, | 
		
	
		
			
				|  |  |  |  |       }; | 
		
	
		
			
				|  |  |  |  |       // assume regular url | 
		
	
		
			
				|  |  |  |  |     } else { | 
		
	
		
			
				|  |  |  |  |       ret = { | 
		
	
		
			
				|  |  |  |  |     const [, user, gistId] = link.match(RE_GH_GIST)!; | 
		
	
		
			
				|  |  |  |  |     const safeURL = sanitizeHTMLAttribute( | 
		
	
		
			
				|  |  |  |  |       `https://gist.github.com/${user}/${gistId}`, | 
		
	
		
			
				|  |  |  |  |     ); | 
		
	
		
			
				|  |  |  |  |     const ret: EmbeddedLink = { | 
		
	
		
			
				|  |  |  |  |       type: "document", | 
		
	
		
			
				|  |  |  |  |       srcdoc: () => | 
		
	
		
			
				|  |  |  |  |         createSrcDoc(` | 
		
	
		
			
				|  |  |  |  |           <script src="${link}.js"></script> | 
		
	
		
			
				|  |  |  |  |           <script src="${safeURL}.js"></script> | 
		
	
		
			
				|  |  |  |  |           <style type="text/css"> | 
		
	
		
			
				|  |  |  |  |             * { margin: 0px; } | 
		
	
		
			
				|  |  |  |  |             table, .gist { height: 100%; } | 
		
	
	
		
			
				
					
					|  |  |  | @@ -185,7 +179,6 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => { | 
		
	
		
			
				|  |  |  |  |         `), | 
		
	
		
			
				|  |  |  |  |       aspectRatio: { w: 550, h: 720 }, | 
		
	
		
			
				|  |  |  |  |     }; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |     embeddedLinkCache.set(link, ret); | 
		
	
		
			
				|  |  |  |  |     return ret; | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
	
		
			
				
					
					|  |  |  |   |