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

This commit is contained in:
David Luzar 2024-01-04 13:27:52 +01:00 committed by GitHub
parent 4249b7dec8
commit 43ccc875fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 63 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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