Browse Source

feat: support multi-embed pasting & x.com domain (#7516)

David Luzar 1 year ago
parent
commit
43ccc875fb

+ 43 - 13
packages/excalidraw/components/App.tsx

@@ -182,6 +182,7 @@ import {
   ExcalidrawIframeLikeElement,
   IframeData,
   ExcalidrawIframeElement,
+  ExcalidrawEmbeddableElement,
 } from "../element/types";
 import { getCenter, getDistance } from "../gesture";
 import {
@@ -271,11 +272,12 @@ import {
   easeOut,
   updateStable,
   addEventListener,
+  normalizeEOL,
 } from "../utils";
 import {
   createSrcDoc,
   embeddableURLValidator,
-  extractSrc,
+  maybeParseEmbedSrc,
   getEmbedLink,
 } from "../element/embeddable";
 import {
@@ -2924,21 +2926,49 @@ class App extends React.Component<AppProps, AppState> {
           retainSeed: isPlainPaste,
         });
       } else if (data.text) {
-        const maybeUrl = extractSrc(data.text);
+        const nonEmptyLines = normalizeEOL(data.text)
+          .split(/\n+/)
+          .map((s) => s.trim())
+          .filter(Boolean);
+
+        const embbeddableUrls = nonEmptyLines
+          .map((str) => maybeParseEmbedSrc(str))
+          .filter((string) => {
+            return (
+              embeddableURLValidator(string, this.props.validateEmbeddable) &&
+              (/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(string) ||
+                getEmbedLink(string)?.type === "video")
+            );
+          });
 
         if (
-          !isPlainPaste &&
-          embeddableURLValidator(maybeUrl, this.props.validateEmbeddable) &&
-          (/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(maybeUrl) ||
-            getEmbedLink(maybeUrl)?.type === "video")
+          !IS_PLAIN_PASTE &&
+          embbeddableUrls.length > 0 &&
+          // if there were non-embeddable text (lines) mixed in with embeddable
+          // urls, ignore and paste as text
+          embbeddableUrls.length === nonEmptyLines.length
         ) {
-          const embeddable = this.insertEmbeddableElement({
-            sceneX,
-            sceneY,
-            link: normalizeLink(maybeUrl),
-          });
-          if (embeddable) {
-            this.setState({ selectedElementIds: { [embeddable.id]: true } });
+          const embeddables: NonDeleted<ExcalidrawEmbeddableElement>[] = [];
+          for (const url of embbeddableUrls) {
+            const prevEmbeddable: ExcalidrawEmbeddableElement | undefined =
+              embeddables[embeddables.length - 1];
+            const embeddable = this.insertEmbeddableElement({
+              sceneX: prevEmbeddable
+                ? prevEmbeddable.x + prevEmbeddable.width + 20
+                : sceneX,
+              sceneY,
+              link: normalizeLink(url),
+            });
+            if (embeddable) {
+              embeddables.push(embeddable);
+            }
+          }
+          if (embeddables.length) {
+            this.setState({
+              selectedElementIds: Object.fromEntries(
+                embeddables.map((embeddable) => [embeddable.id, true]),
+              ),
+            });
           }
           return;
         }

+ 13 - 9
packages/excalidraw/element/embeddable.ts

@@ -32,9 +32,9 @@ const RE_GH_GIST_EMBED =
   /^<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 = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:twitter|x).com/;
 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_$]+/;
@@ -54,6 +54,7 @@ const ALLOWED_DOMAINS = new Set([
   "link.excalidraw.com",
   "gist.github.com",
   "twitter.com",
+  "x.com",
   "*.simplepdf.eu",
   "stackblitz.com",
   "val.town",
@@ -155,6 +156,9 @@ export const getEmbedLink = (
   }
 
   if (RE_TWITTER.test(link)) {
+    // the embed srcdoc still supports twitter.com domain only
+    link = link.replace(/\bx.com\b/, "twitter.com");
+
     let ret: IframeData;
     // assume embed code
     if (/<blockquote/.test(link)) {
@@ -321,26 +325,26 @@ const validateHostname = (
   return false;
 };
 
-export const extractSrc = (htmlString: string): string => {
-  const twitterMatch = htmlString.match(RE_TWITTER_EMBED);
+export const maybeParseEmbedSrc = (str: string): string => {
+  const twitterMatch = str.match(RE_TWITTER_EMBED);
   if (twitterMatch && twitterMatch.length === 2) {
     return twitterMatch[1];
   }
 
-  const gistMatch = htmlString.match(RE_GH_GIST_EMBED);
+  const gistMatch = str.match(RE_GH_GIST_EMBED);
   if (gistMatch && gistMatch.length === 2) {
     return gistMatch[1];
   }
 
-  if (RE_GIPHY.test(htmlString)) {
-    return `https://giphy.com/embed/${RE_GIPHY.exec(htmlString)![1]}`;
+  if (RE_GIPHY.test(str)) {
+    return `https://giphy.com/embed/${RE_GIPHY.exec(str)![1]}`;
   }
 
-  const match = htmlString.match(RE_GENERIC_EMBED);
+  const match = str.match(RE_GENERIC_EMBED);
   if (match && match.length === 2) {
     return match[1];
   }
-  return htmlString;
+  return str;
 };
 
 export const embeddableURLValidator = (

+ 3 - 5
packages/excalidraw/element/textElement.ts

@@ -1,4 +1,4 @@
-import { getFontString, arrayToMap, isTestEnv } from "../utils";
+import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
 import {
   ExcalidrawElement,
   ExcalidrawElementType,
@@ -39,15 +39,13 @@ import { ExtractSetType } from "../utility-types";
 
 export const normalizeText = (text: string) => {
   return (
-    text
+    normalizeEOL(text)
       // replace tabs with spaces so they render and measure correctly
       .replace(/\t/g, "        ")
-      // normalize newlines
-      .replace(/\r?\n|\r/g, "\n")
   );
 };
 
-export const splitIntoLines = (text: string) => {
+const splitIntoLines = (text: string) => {
   return normalizeText(text).split("\n");
 };
 

+ 4 - 0
packages/excalidraw/utils.ts

@@ -1071,3 +1071,7 @@ export function addEventListener(
     target?.removeEventListener?.(type, listener, options);
   };
 }
+
+export const normalizeEOL = (str: string) => {
+  return str.replace(/\r?\n|\r/g, "\n");
+};