Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a727f6c5e2 |
@@ -359,12 +359,18 @@ export const getHoveredElementForBinding = (
|
|||||||
return candidateElements[0];
|
return candidateElements[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer smaller shapes
|
// Prefer closer shapes
|
||||||
return candidateElements
|
return candidateElements
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) => b.width ** 2 + b.height ** 2 - (a.width ** 2 + a.height ** 2),
|
(a, b) =>
|
||||||
|
(isPointInElement(point, a, elementsMap)
|
||||||
|
? Infinity
|
||||||
|
: distanceToElement(a, elementsMap, point)) -
|
||||||
|
(isPointInElement(point, b, elementsMap)
|
||||||
|
? Infinity
|
||||||
|
: distanceToElement(b, elementsMap, point)),
|
||||||
)
|
)
|
||||||
.pop() as NonDeleted<ExcalidrawBindableElement>;
|
.shift() as NonDeleted<ExcalidrawBindableElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHoveredElementForFocusPoint = (
|
export const getHoveredElementForFocusPoint = (
|
||||||
|
|||||||
@@ -130,86 +130,6 @@ const parseGoogleDriveVideoLink = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isGoogleMapsURL = (url: string): boolean => {
|
|
||||||
try {
|
|
||||||
const { hostname, pathname } = new URL(
|
|
||||||
url.startsWith("http") ? url : `https://${url}`,
|
|
||||||
);
|
|
||||||
const bareHostname = hostname.replace(/^www\./, "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
(bareHostname === "google.com" || bareHostname === "maps.google.com") &&
|
|
||||||
(pathname === "/maps" || pathname.startsWith("/maps/"))
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGoogleMapsZoom = (zoomOrDistance: string): string | null => {
|
|
||||||
const match = zoomOrDistance.match(/^(\d+(?:\.\d+)?)(z|km|m)$/);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const value = Number(match[1]);
|
|
||||||
if (match[2] === "z") {
|
|
||||||
return `${Math.round(value)}`;
|
|
||||||
}
|
|
||||||
const meters = value * (match[2] === "km" ? 1000 : 1);
|
|
||||||
return `${Math.max(
|
|
||||||
3,
|
|
||||||
Math.min(21, Math.round(16 - Math.log2(meters / 500))),
|
|
||||||
)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseGoogleMapsLink = (url: string): string | null => {
|
|
||||||
if (!isGoogleMapsURL(url)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
||||||
|
|
||||||
if (
|
|
||||||
urlObj.pathname.startsWith("/maps/embed") ||
|
|
||||||
urlObj.searchParams.get("output") === "embed"
|
|
||||||
) {
|
|
||||||
return urlObj.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, lat, lng, zoomOrDistance] =
|
|
||||||
urlObj.pathname.match(
|
|
||||||
/@(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),([^/?#,]+)/,
|
|
||||||
) || [];
|
|
||||||
const place = urlObj.pathname.match(/^\/maps\/place\/([^/]+)/)?.[1];
|
|
||||||
const query =
|
|
||||||
urlObj.searchParams.get("q") ||
|
|
||||||
(place ? decodeURIComponent(place).replace(/\+/g, " ") : null) ||
|
|
||||||
(lat && lng ? `${lat},${lng}` : null);
|
|
||||||
|
|
||||||
if (!query) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const embedURL = new URL("https://www.google.com/maps");
|
|
||||||
embedURL.searchParams.set("q", query);
|
|
||||||
embedURL.searchParams.set("output", "embed");
|
|
||||||
|
|
||||||
if (lat && lng) {
|
|
||||||
embedURL.searchParams.set("ll", `${lat},${lng}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoom = zoomOrDistance ? getGoogleMapsZoom(zoomOrDistance) : null;
|
|
||||||
if (zoom) {
|
|
||||||
embedURL.searchParams.set("z", zoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
return embedURL.toString();
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ALLOWED_DOMAINS = new Set([
|
const ALLOWED_DOMAINS = new Set([
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
"youtu.be",
|
"youtu.be",
|
||||||
@@ -227,8 +147,6 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"giphy.com",
|
"giphy.com",
|
||||||
"reddit.com",
|
"reddit.com",
|
||||||
"forms.microsoft.com",
|
"forms.microsoft.com",
|
||||||
"wikipedia.org",
|
|
||||||
"*.wikipedia.org",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ALLOW_SAME_ORIGIN = new Set([
|
const ALLOW_SAME_ORIGIN = new Set([
|
||||||
@@ -360,27 +278,6 @@ export const getEmbedLink = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGoogleMapsURL(link)) {
|
|
||||||
const googleMapsLink = parseGoogleMapsLink(link);
|
|
||||||
if (googleMapsLink) {
|
|
||||||
link = googleMapsLink;
|
|
||||||
aspectRatio = { w: 600, h: 450 };
|
|
||||||
embeddedLinkCache.set(originalLink, {
|
|
||||||
link,
|
|
||||||
intrinsicSize: aspectRatio,
|
|
||||||
type,
|
|
||||||
sandbox: { allowSameOrigin },
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
link,
|
|
||||||
intrinsicSize: aspectRatio,
|
|
||||||
type,
|
|
||||||
sandbox: { allowSameOrigin },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const figmaLink = link.match(RE_FIGMA);
|
const figmaLink = link.match(RE_FIGMA);
|
||||||
if (figmaLink) {
|
if (figmaLink) {
|
||||||
type = "generic";
|
type = "generic";
|
||||||
@@ -609,7 +506,6 @@ 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);
|
||||||
@@ -635,5 +531,5 @@ export const embeddableURLValidator = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isGoogleMapsURL(url) || !!matchHostname(url, ALLOWED_DOMAINS);
|
return !!matchHostname(url, ALLOWED_DOMAINS);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { embeddableURLValidator, getEmbedLink } from "../src/embeddable";
|
||||||
embeddableURLValidator,
|
|
||||||
getEmbedLink,
|
|
||||||
maybeParseEmbedSrc,
|
|
||||||
} from "../src/embeddable";
|
|
||||||
|
|
||||||
describe("YouTube timestamp parsing", () => {
|
describe("YouTube timestamp parsing", () => {
|
||||||
it("should parse YouTube URLs with timestamp in seconds", () => {
|
it("should parse YouTube URLs with timestamp in seconds", () => {
|
||||||
@@ -235,62 +231,3 @@ describe("Google Drive video embedding", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Google Maps embedding", () => {
|
|
||||||
const regularUrl =
|
|
||||||
"https://www.google.com/maps/place/26-432+Jab%C5%82onica,+Poland/@51.356302,20.797168,1921m/data=!3m2!1e3!4b1!4m15!1m8!3m7!1s0x47186c0e0e7578fd:0xe80d19a1ef6ad853!2zMjctMTAwIEnFgsW8YSwgUG9sYW5k!3b1!8m2!3d51.16305!4d21.23991!16zL20vMGM1ZnJ3!3m5!1s0x47184db43a4a5df9:0x6a2b8e648f9dc694!8m2!3d51.3562959!4d20.8023178!16s%2Fm%2F04q6t9r?entry=ttu";
|
|
||||||
const officialEmbedSrc =
|
|
||||||
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d8363.540754033738!2d20.79716795156659!3d51.356301987021546!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47184db43a4a5df9%3A0x6a2b8e648f9dc694!2s26-432%20Jab%C5%82onica%2C%20Poland!5e1!3m2!1sen!2scz!4v1778159513974!5m2!1sen!2scz";
|
|
||||||
|
|
||||||
it("should preserve official Google Maps embed links", () => {
|
|
||||||
const parsedSrc = maybeParseEmbedSrc(
|
|
||||||
`<iframe src="${officialEmbedSrc}" width="600" height="450"></iframe>`,
|
|
||||||
);
|
|
||||||
const result = getEmbedLink(parsedSrc);
|
|
||||||
|
|
||||||
expect(embeddableURLValidator(parsedSrc, undefined)).toBe(true);
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result?.type).toBe("generic");
|
|
||||||
if (result?.type === "generic") {
|
|
||||||
expect(result.link).toBe(officialEmbedSrc);
|
|
||||||
}
|
|
||||||
expect(result?.intrinsicSize).toEqual({ w: 600, h: 450 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should normalize regular Google Maps place links", () => {
|
|
||||||
const result = getEmbedLink(regularUrl);
|
|
||||||
|
|
||||||
expect(embeddableURLValidator(regularUrl, undefined)).toBe(true);
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result?.type).toBe("generic");
|
|
||||||
if (result?.type !== "generic") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const embedURL = new URL(result.link);
|
|
||||||
expect(embedURL.origin).toBe("https://www.google.com");
|
|
||||||
expect(embedURL.pathname).toBe("/maps");
|
|
||||||
expect(embedURL.searchParams.get("q")).toBe(
|
|
||||||
decodeURIComponent("26-432%20Jab%C5%82onica%2C%20Poland"),
|
|
||||||
);
|
|
||||||
expect(embedURL.searchParams.get("output")).toBe("embed");
|
|
||||||
expect(embedURL.searchParams.get("ll")).toBe("51.356302,20.797168");
|
|
||||||
expect(embedURL.searchParams.get("z")).toBe("14");
|
|
||||||
expect(result.intrinsicSize).toEqual({ w: 600, h: 450 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject non-Maps Google pages and fail closed for unsupported Maps pages", () => {
|
|
||||||
expect(
|
|
||||||
embeddableURLValidator("https://www.google.com/search?q=maps", undefined),
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
const unsupportedMapsUrl = "https://www.google.com/maps/about/";
|
|
||||||
expect(embeddableURLValidator(unsupportedMapsUrl, undefined)).toBe(true);
|
|
||||||
expect(getEmbedLink(unsupportedMapsUrl)).toBe(null);
|
|
||||||
|
|
||||||
const malformedMapsUrl = `https://www.google.com/maps/@${"0,0,".repeat(
|
|
||||||
1000,
|
|
||||||
)}`;
|
|
||||||
expect(embeddableURLValidator(malformedMapsUrl, undefined)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1840,6 +1840,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
src={
|
src={
|
||||||
src?.type !== "document" ? src?.link ?? "" : undefined
|
src?.type !== "document" ? src?.link ?? "" : undefined
|
||||||
}
|
}
|
||||||
|
// https://stackoverflow.com/q/18470015
|
||||||
|
scrolling="no"
|
||||||
referrerPolicy="no-referrer-when-downgrade"
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
title="Excalidraw Embedded Content"
|
title="Excalidraw Embedded Content"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
|||||||
Reference in New Issue
Block a user