fix: Enable embeds within HTML and PDF exports (#6290)

This commit is contained in:
Tom Moor
2023-12-14 21:52:51 -05:00
committed by GitHub
parent c40ab288fa
commit bd65a4f151
101 changed files with 420 additions and 2276 deletions

View File

@@ -1,6 +1,6 @@
import find from "lodash/find";
import * as React from "react";
import embeds, { EmbedDescriptor } from "@shared/editor/embeds";
import embeds from "@shared/editor/embeds";
import { IntegrationType } from "@shared/types";
import Integration from "~/models/Integration";
import Logger from "~/utils/Logger";
@@ -9,8 +9,7 @@ import useStores from "./useStores";
/**
* Hook to get all embed configuration for the current team
*
* @param loadIfMissing Should we load integration settings if they are not
* locally available
* @param loadIfMissing Should we load integration settings if they are not locally available
* @returns A list of embed descriptors
*/
export default function useEmbeds(loadIfMissing = false) {
@@ -36,14 +35,18 @@ export default function useEmbeds(loadIfMissing = false) {
return React.useMemo(
() =>
embeds.map((e) => {
const em: Integration<IntegrationType.Embed> | undefined = find(
integrations.orderedData,
(i) => i.service === e.component.name.toLowerCase()
);
return new EmbedDescriptor({
...e,
settings: em?.settings,
});
// Find any integrations that match this embed and inject the settings
const integration: Integration<IntegrationType.Embed> | undefined =
find(
integrations.orderedData,
(integration) => integration.service === e.name
);
if (integration?.settings) {
e.settings = integration.settings;
}
return e;
}),
[integrations.orderedData]
);

View File

@@ -4,10 +4,12 @@ import { DefaultTheme, ThemeProps } from "styled-components";
import { EmbedProps as Props } from "../embeds";
import Widget from "./Widget";
export default function DisabledEmbed(props: Props & ThemeProps<DefaultTheme>) {
export default function DisabledEmbed(
props: Omit<Props, "matches"> & ThemeProps<DefaultTheme>
) {
return (
<Widget
title={props.embed.title}
title={props.embed.name}
href={props.attrs.href}
icon={props.embed.icon}
context={props.attrs.href.replace(/^https?:\/\//, "")}

View File

@@ -0,0 +1,73 @@
import * as React from "react";
import { EmbedDescriptor } from "../embeds";
import { getMatchingEmbed } from "../lib/embeds";
import { ComponentProps } from "../types";
import DisabledEmbed from "./DisabledEmbed";
import Frame from "./Frame";
const EmbedComponent = ({
isEditable,
isSelected,
theme,
node,
embeds,
embedsDisabled,
}: ComponentProps & {
embeds: EmbedDescriptor[];
embedsDisabled?: boolean;
}) => {
const cache = React.useMemo(
() => getMatchingEmbed(embeds, node.attrs.href),
[embeds, node.attrs.href]
);
if (!cache) {
return null;
}
const { embed, matches } = cache;
if (embedsDisabled) {
return (
<DisabledEmbed
attrs={node.attrs.href}
embed={embed}
isEditable={isEditable}
isSelected={isSelected}
theme={theme}
/>
);
}
if (embed.transformMatch) {
const src = embed.transformMatch(matches);
return (
<Frame
src={src}
isSelected={isSelected}
canonicalUrl={node.attrs.href}
title={embed.title}
referrerPolicy="origin"
border
/>
);
}
if ("component" in embed) {
return (
// @ts-expect-error Component type
<embed.component
attrs={node.attrs}
matches={matches}
isEditable={isEditable}
isSelected={isSelected}
embed={embed}
theme={theme}
/>
);
}
return null;
};
export default EmbedComponent;

View File

@@ -403,6 +403,13 @@ li {
position: relative;
}
iframe.embed {
width: 100%;
height: 400px;
border: 1px solid ${props.theme.embedBorder};
border-radius: 6px;
}
.image,
.video {
line-height: 0;

View File

@@ -1,60 +0,0 @@
import Abstract from "./Abstract";
describe("Abstract", () => {
const match = Abstract.ENABLED[0];
const match2 = Abstract.ENABLED[1];
test("to be enabled on share subdomain link", () => {
expect(
"https://share.goabstract.com/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match
)
).toBeTruthy();
expect(
"https://share.abstract.com/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match
)
).toBeTruthy();
});
test("to be enabled on share link", () => {
expect(
"https://app.goabstract.com/share/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match2
)
).toBeTruthy();
expect(
"https://app.abstract.com/share/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match2
)
).toBeTruthy();
});
test("to be enabled on embed link", () => {
expect(
"https://app.goabstract.com/embed/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match2
)
).toBeTruthy();
expect(
"https://app.abstract.com/embed/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match2
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://sharedgoabstract.com/f473".match(match)).toBe(null);
expect("https://share.goabstractacom/f473".match(match)).toBe(null);
expect("https://app1goabstract.com/share/f473".match(match2)).toBe(null);
expect("https://app.goabstractacom/share/f473".match(match2)).toBe(null);
expect("https://abstract.com".match(match)).toBe(null);
expect("https://goabstract.com".match(match)).toBe(null);
expect("https://app.goabstract.com".match(match)).toBe(null);
expect("https://abstract.com/features".match(match)).toBe(null);
expect("https://app.abstract.com/home".match(match)).toBe(null);
expect("https://abstract.com/pricing".match(match)).toBe(null);
expect("https://goabstract.com/pricing".match(match)).toBe(null);
expect("https://www.goabstract.com/pricing".match(match)).toBe(null);
});
});

View File

@@ -1,22 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Abstract(props: Props) {
const { matches } = props.attrs;
const shareId = matches[1];
return (
<Frame
{...props}
src={`https://app.goabstract.com/embed/${shareId}`}
title={`Abstract (${shareId})`}
/>
);
}
Abstract.ENABLED = [
new RegExp("^https?://share\\.(?:go)?abstract\\.com/(.*)$"),
new RegExp("^https?://app\\.(?:go)?abstract\\.com/(?:share|embed)/(.*)$"),
];
export default Abstract;

View File

@@ -1,29 +0,0 @@
import Airtable from "./Airtable";
describe("Airtable", () => {
const match = Airtable.ENABLED[0];
test("to be enabled on share link", () => {
expect("https://airtable.com/shrEoQs3erLnppMie".match(match)).toBeTruthy();
expect(
"https://airtable.com/shrEoQs3erLnppMie/tbl232i54yebXpr1J".match(match)
).toBeTruthy();
expect(
"https://airtable.com/app0lrLlMprqMbz11/shrEoQs3erLnppMie/tbl232i54yebXpr1J".match(
match
)
).toBeTruthy();
});
test("to be enabled on embed link", () => {
expect(
"https://airtable.com/embed/shrEoQs3erLnppMie".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://airtable.com".match(match)).toBe(null);
expect("https://airtable.com/features".match(match)).toBe(null);
expect("https://airtable.com/pricing".match(match)).toBe(null);
});
});

View File

@@ -1,26 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
const URL_REGEX = new RegExp(
"^https://airtable.com/(?:embed/)?(app.*/)?(shr.*)$"
);
function Airtable(props: Props) {
const { matches } = props.attrs;
const appId = matches[1];
const shareId = matches[2];
return (
<Frame
{...props}
src={`https://airtable.com/embed/${appId ?? ""}${shareId}`}
title={`Airtable (${shareId})`}
border
/>
);
}
Airtable.ENABLED = [URL_REGEX];
export default Airtable;

View File

@@ -1,8 +0,0 @@
import Berrycast from "./Berrycast";
describe("Berrycast", () => {
Berrycast.ENABLED[0];
// TODO
test.todo("");
});

View File

@@ -3,9 +3,7 @@ import Frame from "../components/Frame";
import useComponentSize from "../components/hooks/useComponentSize";
import { EmbedProps as Props } from ".";
const URL_REGEX = /^https:\/\/(www\.)?berrycast.com\/conversations\/(.*)$/;
export default function Berrycast(props: Props) {
export default function Berrycast({ matches, ...props }: Props) {
const normalizedUrl = props.attrs.href.replace(/\/$/, "");
const ref = React.useRef<HTMLDivElement>(null);
const { width } = useComponentSize(ref.current);
@@ -23,5 +21,3 @@ export default function Berrycast(props: Props) {
</>
);
}
Berrycast.ENABLED = [URL_REGEX];

View File

@@ -1,23 +0,0 @@
import Bilibili from "./Bilibili";
describe("Bilibili", () => {
const match = Bilibili.ENABLED[0];
test("to be enabled on video link", () => {
expect(
"https://www.bilibili.com/video/BV1CV411s7jd?spm_id_from=333.999.0.0".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://youtu.be".match(match)).toBe(null);
expect("https://bilibili.com".match(match)).toBe(null);
expect("https://www.bilibili.com".match(match)).toBe(null);
expect("https://www.bilibili.com/logout".match(match)).toBe(null);
expect("https://www.bilibili.com/feed/subscriptions".match(match)).toBe(
null
);
});
});

View File

@@ -1,20 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
const URL_REGEX =
/(?:https?:\/\/)?(www\.bilibili\.com)\/video\/([\w\d]+)?(\?\S+)?/i;
export default function Bilibili(props: Props) {
const { matches } = props.attrs;
const videoId = matches[2];
return (
<Frame
{...props}
src={`https://player.bilibili.com/player.html?bvid=${videoId}&page=1&high_quality=1`}
title={`Bilibili Embed (${videoId})`}
/>
);
}
Bilibili.ENABLED = [URL_REGEX];

View File

@@ -1,22 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Canva(props: Props) {
const { matches } = props.attrs;
const embedId = matches[1];
return (
<Frame
{...props}
src={`https://www.canva.com/design/${embedId}/view?embed`}
title="Canva"
/>
);
}
Canva.ENABLED = [
/^https:\/\/(?:www\.)?canva\.com\/design\/([a-zA-Z0-9]*)\/(.*)$/,
];
export default Canva;

View File

@@ -1,22 +0,0 @@
import Cawemo from "./Cawemo";
describe("Cawemo", () => {
const match = Cawemo.ENABLED[0];
test("to be enabled on embed link", () => {
expect(
"https://cawemo.com/embed/a82e9f22-e283-4253-8d11".match(match)
).toBeTruthy();
});
test("to be enabled on share link", () => {
expect(
"https://cawemo.com/embed/a82e9f22-e283-4253-8d11".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://cawemo.com/".match(match)).toBe(null);
expect("https://cawemo.com/diagrams".match(match)).toBe(null);
});
});

View File

@@ -1,20 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
const URL_REGEX = new RegExp("^https?://cawemo.com/(?:share|embed)/(.*)$");
export default function Cawemo(props: Props) {
const { matches } = props.attrs;
const shareId = matches[1];
return (
<Frame
{...props}
src={`https://cawemo.com/embed/${shareId}`}
title={"Cawemo Embed"}
border
/>
);
}
Cawemo.ENABLED = [URL_REGEX];

View File

@@ -1,19 +0,0 @@
import ClickUp from "./ClickUp";
describe("ClickUp", () => {
const match = ClickUp.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://share.clickup.com/b/h/6-9310960-2/c9d837d74182317".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://share.clickup.com".match(match)).toBe(null);
expect("https://sharedclickup.com/a/b/c/d".match(match)).toBe(null);
expect("https://share.clickupdcom/a/b/c/d".match(match)).toBe(null);
expect("https://clickup.com/".match(match)).toBe(null);
expect("https://clickup.com/features".match(match)).toBe(null);
});
});

View File

@@ -1,12 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
export default function ClickUp(props: Props) {
return <Frame {...props} src={props.attrs.href} title="ClickUp Embed" />;
}
ClickUp.ENABLED = [
new RegExp("^https?://share\\.clickup\\.com/[a-z]/[a-z]/(.*)/(.*)$"),
new RegExp("^https?://sharing\\.clickup\\.com/[0-9]+/[a-z]/[a-z]/(.*)/(.*)$"),
];

View File

@@ -1,22 +0,0 @@
import Codepen from "./Codepen";
describe("Codepen", () => {
const match = Codepen.ENABLED[0];
test("to be enabled on pen link", () => {
expect(
"https://codepen.io/chriscoyier/pen/gfdDu".match(match)
).toBeTruthy();
});
test("to be enabled on embed link", () => {
expect(
"https://codepen.io/chriscoyier/embed/gfdDu".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://codepen.io".match(match)).toBe(null);
expect("https://codepen.io/chriscoyier".match(match)).toBe(null);
});
});

View File

@@ -1,12 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
const URL_REGEX = new RegExp("^https://codepen.io/(.*?)/(pen|embed)/(.*)$");
export default function Codepen(props: Props) {
const normalizedUrl = props.attrs.href.replace(/\/pen\//, "/embed/");
return <Frame {...props} src={normalizedUrl} title="Codepen Embed" />;
}
Codepen.ENABLED = [URL_REGEX];

View File

@@ -1,8 +0,0 @@
import DBDiagram from "./DBDiagram";
describe("DBDiagram", () => {
DBDiagram.ENABLED[0];
// TODO
test.todo("");
});

View File

@@ -1,20 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
export default function DBDiagram(props: Props) {
const { matches } = props.attrs;
const shareId = matches[2];
return (
<Frame
{...props}
src={`https://dbdiagram.io/embed/${shareId}`}
title={`DBDiagram (${shareId})`}
width="100%"
height="400px"
/>
);
}
DBDiagram.ENABLED = [new RegExp("^https://dbdiagram.io/(embed|d)/(\\w+)$")];

View File

@@ -1,10 +0,0 @@
import Descript from "./Descript";
describe("Descript", () => {
const match = Descript.ENABLED[0];
test("to not be enabled elsewhere", () => {
expect("https://shareddescript.com/view/c9d8".match(match)).toBe(null);
expect("https://share.descriptdcom/view/c9d8".match(match)).toBe(null);
});
});

View File

@@ -1,20 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
export default function Descript(props: Props) {
const { matches } = props.attrs;
const shareId = matches[1];
return (
<Frame
{...props}
src={`https://share.descript.com/embed/${shareId}`}
title={`Descript (${shareId})`}
width="400px"
/>
);
}
Descript.ENABLED = [
new RegExp("^https?://share\\.descript\\.com/view/(\\w+)$"),
];

View File

@@ -1,25 +0,0 @@
import Diagrams from "./Diagrams";
describe("Diagrams", () => {
const match = Diagrams.ENABLED[0];
test("to be enabled on viewer link", () => {
expect(
"https://viewer.diagrams.net/?target=blank&nav=1#ABCDefgh_A12345-6789".match(
match
)
).toBeTruthy();
});
test("to not be enabled on the proxy path", () => {
expect("https://app.diagrams.net/proxy?url=malicious".match(match)).toBe(
null
);
});
test("to not be enabled elsewhere", () => {
expect("https://app.diagrams.net/#ABCDefgh_A12345-6789".match(match)).toBe(
null
);
});
});

View File

@@ -3,9 +3,9 @@ import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function Diagrams(props: Props) {
function Diagrams({ matches, ...props }: Props) {
const { embed } = props;
const embedUrl = props.attrs.matches[0];
const embedUrl = matches[0];
const params = new URL(embedUrl).searchParams;
const titlePrefix = embed.settings?.url ? "Draw.io" : "Diagrams.net";
const title = params.get("title")
@@ -31,10 +31,4 @@ function Diagrams(props: Props) {
);
}
Diagrams.ENABLED = [
/^https:\/\/viewer\.diagrams\.net\/(?!proxy).*(title=\\w+)?/,
];
Diagrams.URL_PATH_REGEX = /\/(?!proxy).*(title=\\w+)?/;
export default Diagrams;

View File

@@ -1,28 +0,0 @@
import Figma from "./Figma";
describe("Figma", () => {
const match = Figma.ENABLED[0];
test("to be enabled on file link", () => {
expect(
"https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931".match(match)
).toBeTruthy();
});
test("to be enabled on prototype link", () => {
expect(
"https://www.figma.com/proto/LKQ4FJ4bTnCSjedbRpk931".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://www.figma.com".match(match)).toBe(null);
expect("https://www.figma.com/features".match(match)).toBe(null);
expect(
"https://wwww.figmaacom/file/LKQ4FJ4bTnCSjedbRpk931".match(match)
).toBe(null);
expect(
"https://wwwwfigma.com/file/LKQ4FJ4bTnCSjedbRpk931".match(match)
).toBe(null);
});
});

View File

@@ -1,22 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Figma(props: Props) {
return (
<Frame
{...props}
src={`https://www.figma.com/embed?embed_host=outline&url=${props.attrs.href}`}
title="Figma Embed"
border
/>
);
}
Figma.ENABLED = [
new RegExp(
"^https://([w.-]+\\.)?figma\\.com/(file|proto)/([0-9a-zA-Z]{22,128})(?:/.*)?$"
),
];
export default Figma;

View File

@@ -1,13 +0,0 @@
import Framer from "./Framer";
describe("Framer", () => {
const match = Framer.ENABLED[0];
test("to be enabled on share link", () => {
expect("https://framer.cloud/PVwJO".match(match)).toBeTruthy();
});
test("to not be enabled on root", () => {
expect("https://framer.cloud".match(match)).toBe(null);
});
});

View File

@@ -1,13 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Framer(props: Props) {
return (
<Frame {...props} src={props.attrs.href} title="Framer Embed" border />
);
}
Framer.ENABLED = [new RegExp("^https://framer.cloud/(.*)$")];
export default Framer;

View File

@@ -1,37 +0,0 @@
import Gist from "./Gist";
describe("Gist", () => {
const match = Gist.ENABLED[0];
test("to be enabled on gist link", () => {
expect(
"https://gist.github.com/wmertens/0b4fd66ca7055fd290ecc4b9d95271a9".match(
match
)
).toBeTruthy();
expect(
"https://gist.github.com/n3n/eb51ada6308b539d388c8ff97711adfa".match(
match
)
).toBeTruthy();
expect(
"https://gist.github.com/ShubhanjanMedhi-dev/900c9c14093611898a4a085938bb90d9".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect(
"https://gistigithub.com/n3n/eb51ada6308b539d388c8ff97711adfa".match(
match
)
).toBe(null);
expect(
"https://gist.githubbcom/n3n/eb51ada6308b539d388c8ff97711adfa".match(
match
)
).toBe(null);
expect("https://gist.github.com/tommoor".match(match)).toBe(null);
});
});

View File

@@ -29,10 +29,4 @@ function Gist(props: Props) {
);
}
Gist.ENABLED = [
new RegExp(
"^https://gist\\.github\\.com/([a-zA-Z\\d](?:[a-zA-Z\\d]|-(?=[a-zA-Z\\d])){0,38})/(.*)$"
),
];
export default Gist;

View File

@@ -1,31 +0,0 @@
import GitLabSnippet from "./GitLabSnippet";
describe("GitLabSnippet", () => {
const match = GitLabSnippet.ENABLED[0];
test("to be enabled on snippet link", () => {
expect("https://gitlab.com/-/snippets/1234".match(match)).toBeTruthy();
expect(
"https://gitlab.com/gitlab-org/gitlab/-/snippets/2256824".match(match)
).toBeTruthy();
expect(
"https://gitlab.com/group/project/sub-project/sub-sub-project/test/-/snippets/9876".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://gitlab.com".match(match)).toBe(null);
expect("https://gitlab.com/gitlab-org".match(match)).toBe(null);
expect("https://gitlab.com/gitlab-org/gitlab".match(match)).toBe(null);
expect("https://gitlab.com/gitlab-org/gitlab/-/issues".match(match)).toBe(
null
);
expect(
"https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137948".match(
match
)
).toBe(null);
});
});

View File

@@ -2,8 +2,6 @@ import * as React from "react";
import styled from "styled-components";
import { EmbedProps as Props } from ".";
const GITLAB_NAMESPACE_REGEX = "(([a-zA-Z\\d-]+)/)";
const Iframe = styled.iframe`
margin-top: 8px;
`;
@@ -29,10 +27,4 @@ function GitLabSnippet(props: Props) {
);
}
GitLabSnippet.ENABLED = [
new RegExp(
`^https://gitlab\\.com/${GITLAB_NAMESPACE_REGEX}*-/snippets/\\d+$`
),
];
export default GitLabSnippet;

View File

@@ -1,14 +0,0 @@
import Gliffy from "./Gliffy";
describe("Gliffy", () => {
const match = Gliffy.ENABLED[0];
test("to not be enabled elsewhere", () => {
expect("https://gotgliffy.com/go/share/c9d837d74182317".match(match)).toBe(
null
);
expect("https://go.gliffyycom/go/share/c9d837d74182317".match(match)).toBe(
null
);
});
});

View File

@@ -1,13 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Gliffy(props: Props) {
return (
<Frame {...props} src={props.attrs.href} title="Gliffy Embed" border />
);
}
Gliffy.ENABLED = [new RegExp("https?://go\\.gliffy\\.com/go/share/(.*)$")];
export default Gliffy;

View File

@@ -1,29 +0,0 @@
import GoogleCalendar from "./GoogleCalendar";
describe("GoogleCalendar", () => {
const match = GoogleCalendar.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://calendar.google.com/calendar/embed?src=tom%40outline.com&ctz=America%2FSao_Paulo".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://calendar.google.com/calendar".match(match)).toBe(null);
expect("https://calendar.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
expect(
"https://calendarrgoogle.com/calendar/embed?src=tom%40outline.com&ctz=America%2FSao_Paulo".match(
match
)
).toBe(null);
expect(
"https://calendar.googleecom/calendar/embed?src=tom%40outline.com&ctz=America%2FSao_Paulo".match(
match
)
).toBe(null);
});
});

View File

@@ -1,15 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function GoogleCalendar(props: Props) {
return (
<Frame {...props} src={props.attrs.href} title="Google Calendar" border />
);
}
GoogleCalendar.ENABLED = [
new RegExp("^https?://calendar\\.google\\.com/calendar/embed\\?src=(.*)$"),
];
export default GoogleCalendar;

View File

@@ -1,44 +0,0 @@
import GoogleDocs from "./GoogleDocs";
describe("GoogleDocs", () => {
const match = GoogleDocs.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pubhtml".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/document/d/1SsDfWzFFTjZM2LanvpyUzjKhqVQpwpTMeiPeYxhVqOg/edit".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/document/d/1SsDfWzFFTjZM2LanvpyUzjKhqVQpwpTMeiPeYxhVqOg/preview".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://docs.google.com/document".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
expect(
"https://docssgoogle.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pubhtml".match(
match
)
).toBe(null);
expect(
"https://docs.googleecom/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pubhtml".match(
match
)
).toBe(null);
});
});

View File

@@ -1,30 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function GoogleDocs(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href.replace("/edit", "/preview")}
icon={
<Image
src="/images/google-docs.png"
alt="Google Docs Icon"
width={16}
height={16}
/>
}
canonicalUrl={props.attrs.href}
title="Google Docs"
border
/>
);
}
GoogleDocs.ENABLED = [
new RegExp("^https?://docs\\.google\\.com/document/(.*)$"),
];
export default GoogleDocs;

View File

@@ -1,39 +0,0 @@
import GoogleDrawings from "./GoogleDrawings";
describe("GoogleDrawings", () => {
const match = GoogleDrawings.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/drawings/d/1zELtJ4HSCnjGCGSoCgqGe3F8p6o7R8Vjk8MDR6dKf-U/edit".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/drawings/d/1zELtJ4HSCnjGCGSoCgqGe3F8p6o7R8Vjk8MDR6dKf-U/edit?usp=sharing".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect(
"https://docs.google.com/drawings/d/e/2PACF-1vRtzIzEWN6svSrIYZq-kq2XZEN6WaOFXHbPKRLXNOFRlxLIdJg0Vo6RfretGqs9SzD-fUazLeS594Kw/pub?w=960&h=720".match(
match
)
).toBe(null);
expect("https://docs.google.com/drawings".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
expect(
"https://docssgoogle.com/drawings/d/1zDMtJ4HSCnjGCGSoCgqGe3F8p6o7R8Vjk8MDR6dKf-U/edit".match(
match
)
).toBe(null);
expect(
"https://docs.googleecom/drawings/d/1zDMtJ4HSCnjGCGSoCgqGe3F8p6o7R8Vjk8MDR6dKf-U/edit".match(
match
)
).toBe(null);
});
});

View File

@@ -1,32 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function GoogleDrawings(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href.replace("/edit", "/preview")}
icon={
<Image
src="/images/google-drawings.png"
alt="Google Drawings"
width={16}
height={16}
/>
}
canonicalUrl={props.attrs.href.replace("/preview", "/edit")}
title="Google Drawings"
border
/>
);
}
GoogleDrawings.ENABLED = [
new RegExp(
"^https://docs\\.google\\.com/drawings/d/(.*)/(edit|preview)(.*)$"
),
];
export default GoogleDrawings;

View File

@@ -1,39 +0,0 @@
import GoogleDrive from "./GoogleDrive";
describe("GoogleDrive", () => {
const match = GoogleDrive.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://drive.google.com/file/d/1ohkOgmE8MiNx68u6ynBfYkgjeKu_x3ZK/view?usp=sharing".match(
match
)
).toBeTruthy();
expect(
"https://drive.google.com/file/d/1ohkOgmE8MiNx68u6ynBfYkgjeKu_x3ZK/preview?usp=sharing".match(
match
)
).toBeTruthy();
expect(
"https://drive.google.com/file/d/1ohkOgmE8MiNx68u6ynBfYkgjeKu_x3ZK/preview?usp=sharing&resourceKey=BG8k4dEt1p2gisnVdlaSpA".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://drive.google.com/file".match(match)).toBe(null);
expect("https://drive.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
expect(
"https://driveegoogle.com/file/d/1ohkOgmE8MiNx68u6ynBfYkgjeKu_x3ZK/view?usp=sharing".match(
match
)
).toBe(null);
expect(
"https://drive.googleecom/file/d/1ohkOgmE8MiNx68u6ynBfYkgjeKu_x3ZK/view?usp=sharing".match(
match
)
).toBe(null);
});
});

View File

@@ -1,29 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function GoogleDrive(props: Props) {
return (
<Frame
src={props.attrs.href.replace("/view", "/preview")}
icon={
<Image
src="/images/google-drive.png"
alt="Google Drive Icon"
width={16}
height={16}
/>
}
title="Google Drive"
canonicalUrl={props.attrs.href}
border
/>
);
}
GoogleDrive.ENABLED = [
new RegExp("^https?://drive\\.google\\.com/file/d/(.*)$"),
];
export default GoogleDrive;

View File

@@ -1,48 +0,0 @@
import GoogleForms from "./GoogleForms";
describe("GoogleForms", () => {
const match = GoogleForms.ENABLED[0];
test("to be enabled on long-form share links", () => {
expect(
"https://docs.google.com/forms/d/e/1FAIpQLSetbCGiE8DhfVQZMtLE_CU2MwpSsrkXi690hkEDREOvMu8VYQ/viewform?usp=sf_link".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/forms/d/e/1FAIpQLSetbCGiE8DhfVQZMtLE_CU2MwpSsrkXi690hkEDREOvMu8VYQ/viewform".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/forms/d/e/1FAIpQLSetbCGiE8DhfVQZMtLE_CU2MwpSsrkXi690hkEDREOvMu8VYQ/viewform?embedded=true".match(
match
)
).toBeTruthy();
});
test("to be enabled on edit links", () => {
expect(
"https://docs.google.com/forms/d/1zG75dmHQGpomQlWB6VtRhWajNer7mKMjtApM_aRAJV8/edit".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://docs.google.com/forms".match(match)).toBe(null);
expect("https://docs.google.com/forms/d/".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
expect(
"https://docssgoogle.com/forms/d/e/1FAIpQLSetbCGiE8DhfVQZMtLE_CU2MwpSsrkXi690hkEDREOvMu8VYQ/viewform?usp=sf_link".match(
match
)
).toBe(null);
expect(
"https://docs.googleecom/forms/d/e/1FAIpQLSetbCGiE8DhfVQZMtLE_CU2MwpSsrkXi690hkEDREOvMu8VYQ/viewform".match(
match
)
).toBe(null);
});
});

View File

@@ -1,33 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function GoogleForms(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href.replace(
/\/(edit|viewform)(\?.+)?$/,
"/viewform?embedded=true"
)}
icon={
<Image
src="/images/google-forms.png"
alt="Google Forms Icon"
width={16}
height={16}
/>
}
canonicalUrl={props.attrs.href}
title="Google Forms"
border
/>
);
}
GoogleForms.ENABLED = [
new RegExp("^https?://docs\\.google\\.com/forms/d/(.+)$"),
];
export default GoogleForms;

View File

@@ -1,34 +0,0 @@
import GoogleLookerStudio from "./GoogleLookerStudio";
describe("GoogleLookerStudio", () => {
const match = GoogleLookerStudio.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://lookerstudio.google.com/embed/reporting/aab01789-f3a2-4ff3-9cba-c4c94c4a92e8/page/7zFD".match(
match
)
).toBeTruthy();
expect(
"https://datastudio.google.com/embed/reporting/aab01789-f3a2-4ff3-9cba-c4c94c4a92e8/page/7zFD".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://lookerstudio.google.com/u/0/".match(match)).toBe(null);
expect("https://lookerstudio.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
expect(
"https://lookerstudioogoogle.com/embed/reporting/aab01789-f3a2-4ff3-9cba-c4c94c4a92e8/page/7zFD".match(
match
)
).toBe(null);
expect(
"https://lookerstudio.googleecom/embed/reporting/aab01789-f3a2-4ff3-9cba-c4c94c4a92e8/page/7zFD".match(
match
)
).toBe(null);
});
});

View File

@@ -1,32 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function GoogleLookerStudio(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href.replace("u/0", "embed").replace("/edit", "")}
icon={
<Image
src="/images/google-lookerstudio.png"
alt="Google Looker Studio Icon"
width={16}
height={16}
/>
}
canonicalUrl={props.attrs.href}
title="Google Looker Studio"
border
/>
);
}
GoogleLookerStudio.ENABLED = [
new RegExp(
"^https?://(lookerstudio|datastudio)\\.google\\.com/(embed|u/0)/reporting/(.*)/page/(.*)(/edit)?$"
),
];
export default GoogleLookerStudio;

View File

@@ -1,18 +0,0 @@
import GoogleMaps from "./GoogleMaps";
describe("GoogleMaps", () => {
const match = GoogleMaps.ENABLED[0];
test("to be enabled", () => {
expect(
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d50977.036904273686!2d174.74383592605594!3d-37.00825027293197!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x6d0d4fe87ef3d5bb%3A0xf00ef62249b7130!2sAuckland%20Airport!5e0!3m2!1sen!2snz!4v1691573100204!5m2!1sen!2snz".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://www.google.com/maps/embed".match(match)).toBe(null);
expect("https://goo.gl/maps/".match(match)).toBe(null);
});
});

View File

@@ -1,16 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function GoogleMaps(props: Props) {
const { matches } = props.attrs;
const source = matches[0];
return <Frame {...props} src={source} title="Google Maps" />;
}
GoogleMaps.ENABLED = [
new RegExp("^https?://www\\.google\\.com/maps/embed\\?(.*)$"),
];
export default GoogleMaps;

View File

@@ -1,34 +0,0 @@
import GoogleSheets from "./GoogleSheets";
describe("GoogleSheets", () => {
const match = GoogleSheets.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://docs.google.com/spreadsheets".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
expect(
"https://docssgoogle.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match(
match
)
).toBe(null);
expect(
"https://docs.googleecom/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match(
match
)
).toBe(null);
});
});

View File

@@ -1,30 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function GoogleSheets(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href.replace("/edit", "/preview")}
icon={
<Image
src="/images/google-sheets.png"
alt="Google Sheets Icon"
width={16}
height={16}
/>
}
canonicalUrl={props.attrs.href}
title="Google Sheets"
border
/>
);
}
GoogleSheets.ENABLED = [
new RegExp("^https?://docs\\.google\\.com/spreadsheets/d/(.*)$"),
];
export default GoogleSheets;

View File

@@ -1,39 +0,0 @@
import GoogleSlides from "./GoogleSlides";
describe("GoogleSlides", () => {
const match = GoogleSlides.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub?start=false&loop=false&delayms=3000".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://docs.google.com/presentation".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
expect(
"https://docssgoogle.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub?start=false&loop=false&delayms=3000".match(
match
)
).toBe(null);
expect(
"https://docs.googleecom/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub?start=false&loop=false&delayms=3000".match(
match
)
).toBe(null);
});
});

View File

@@ -1,32 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function GoogleSlides(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href
.replace("/edit", "/preview")
.replace("/pub", "/embed")}
icon={
<Image
src="/images/google-slides.png"
alt="Google Slides Icon"
width={16}
height={16}
/>
}
canonicalUrl={props.attrs.href}
title="Google Slides"
border
/>
);
}
GoogleSlides.ENABLED = [
new RegExp("^https?://docs\\.google\\.com/presentation/d/(.*)$"),
];
export default GoogleSlides;

View File

@@ -1,33 +0,0 @@
import Grist from "./Grist";
describe("Grist", () => {
const match = Grist.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://templates.getgrist.com/doc/afterschool-program/p/2?embed=true".match(
match
)
).toBeTruthy();
expect(
"https://docs.getgrist.com/sg5V93LuAije/Untitled-document/p/22?embed=true".match(
match
)
).toBeTruthy();
expect(
"https://templates.getgrist.com/doc/afterschool-program".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
// Self hosted not yet supported
expect(
"http://grist.my.host.com/o/docs/doc/new~5cCkr6CtMArdA62ohSy5xB/p/1?embed=true".match(
match
)
).toBe(null);
expect(
"https://my.get.grist.com/doc/afterschool-program?embed=true".match(match)
).toBe(null);
});
});

View File

@@ -1,30 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import Image from "../components/Img";
import { EmbedProps as Props } from ".";
function Grist(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href.replace(/(\?embed=true)?$/, "?embed=true")}
icon={
<Image
src="/images/grist.png"
alt="Grist Icon"
width={16}
height={16}
/>
}
title="Grist Spreadsheet"
canonicalUrl={props.attrs.href}
border
/>
);
}
Grist.ENABLED = [new RegExp("^https?://([a-z.-]+\\.)?getgrist\\.com/(.+)$")];
Grist.URL_PATH_REGEX = /(.+)/;
export default Grist;

View File

@@ -1,21 +0,0 @@
import InVision from "./InVision";
describe("InVision", () => {
const match = InVision.ENABLED[0];
test("to be enabled on shortlink", () => {
expect("https://invis.io/69PG07QYQTE".match(match)).toBeTruthy();
});
test("to be enabled on share", () => {
expect(
"https://projects.invisionapp.com/share/69PG07QYQTE".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://invis.io".match(match)).toBe(null);
expect("https://invisionapp.com".match(match)).toBe(null);
expect("https://projects.invisionapp.com".match(match)).toBe(null);
});
});

View File

@@ -3,13 +3,8 @@ import Frame from "../components/Frame";
import ImageZoom from "../components/ImageZoom";
import { EmbedProps as Props } from ".";
const IFRAME_REGEX =
/^https:\/\/(invis\.io\/.*)|(projects\.invisionapp\.com\/share\/.*)$/;
const IMAGE_REGEX =
/^https:\/\/(opal\.invisionapp\.com\/static-signed\/live-embed\/.*)$/;
function InVision(props: Props) {
if (IMAGE_REGEX.test(props.attrs.href)) {
function InVision({ matches, ...props }: Props) {
if (/opal\.invisionapp\.com/.test(props.attrs.href)) {
return (
<div className={props.isSelected ? "ProseMirror-selectednode" : ""}>
<ImageZoom zoomMargin={24}>
@@ -29,6 +24,4 @@ function InVision(props: Props) {
return <Frame {...props} src={props.attrs.href} title="InVision Embed" />;
}
InVision.ENABLED = [IFRAME_REGEX, IMAGE_REGEX];
export default InVision;

View File

@@ -1,27 +0,0 @@
import Instagram from "./Instagram";
describe("Instagram", () => {
const match = Instagram.ENABLED[0];
test("to be enabled on post link", () => {
expect(
"https://www.instagram.com/p/CrL74G6Bxgw/?utm_source=ig_web_copy_link".match(
match
)
).toBeTruthy();
});
test("to be enabled on reel link", () => {
expect(
"https://www.instagram.com/reel/Cxdyt_fMnwN/?utm_source=ig_web_copy_link".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://www.instagram.com".match(match)).toBe(null);
expect("https://www.instagram.com/reel/".match(match)).toBe(null);
expect("https://www.instagram.com/p/".match(match)).toBe(null);
});
});

View File

@@ -1,14 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Instagram(props: Props) {
const { matches } = props.attrs;
return <Frame {...props} src={`${matches[0]}/embed`} title="Instagram" />;
}
Instagram.ENABLED = [
/^https?:\/\/www\.instagram\.com\/(p|reel)\/([\w-]+)(\/?utm_source=\w+)?/,
];
export default Instagram;

View File

@@ -1,11 +0,0 @@
import JSFiddle from "./JSFiddle";
describe("JSFiddle", () => {
const match = JSFiddle.ENABLED[0];
test("to not be enabled for invalid urls", () => {
expect("https://jsfiddleenet/go/share/c9d837d74182317".match(match)).toBe(
null
);
});
});

View File

@@ -3,7 +3,7 @@ import { useTheme } from "styled-components";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function JSFiddle(props: Props) {
function JSFiddle({ matches, ...props }: Props) {
const normalizedUrl = props.attrs.href.replace(/(\/embedded)?\/$/, "");
const theme = useTheme();
@@ -20,6 +20,4 @@ function JSFiddle(props: Props) {
);
}
JSFiddle.ENABLED = [new RegExp("^https?://jsfiddle\\.net/(.*)/(.*)$")];
export default JSFiddle;

View File

@@ -1,39 +0,0 @@
import Linkedin from "./Linkedin";
describe("Linkedin", () => {
const match = Linkedin.ENABLED[0];
test("to be enabled on post link", () => {
expect(
"https://www.linkedin.com/posts/github_highlight-your-expertise-with-github-certifications-activity-7117157514097352704-F4Mz?utm_source=share&utm_medium=member_desktop".match(
match
)
).toBeTruthy();
});
test("to be enabled on embed link", () => {
expect(
"https://www.linkedin.com/embed/feed/update/urn:li:share:7117157513422090241".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://www.linkedin.com/".match(match)).toBe(null);
expect("https://www.linkedin.com/posts/".match(match)).toBe(null);
expect("https://www.linkedin.com/embed/".match(match)).toBe(null);
expect("https://www.linkedin.com/embed/feed/update/".match(match)).toBe(
null
);
expect(
"https://www.linkedin.com/embed/feed/update/urn:li:".match(match)
).toBe(null);
expect(
"https://www.linkedin.com/embed/feed/update/urn:li:share:".match(match)
).toBe(null);
expect(
"https://www.linkedin.com/embed/feed/update/urn:li:ugcPost:".match(match)
).toBe(null);
});
});

View File

@@ -2,8 +2,7 @@ import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Linkedin(props: Props) {
const { matches } = props.attrs;
function Linkedin({ matches, ...props }: Props) {
const objectId = matches[2];
const postType = matches[1];
if (matches[3] === "embed") {
@@ -18,8 +17,4 @@ function Linkedin(props: Props) {
);
}
Linkedin.ENABLED = [
/^https:\/\/www\.linkedin\.com\/(?:posts\/.*-(ugcPost|activity)-(\d+)-.*|(embed)\/(?:feed\/update\/urn:li:(?:ugcPost|share):(?:\d+)))/,
];
export default Linkedin;

View File

@@ -1,35 +0,0 @@
import Loom from "./Loom";
describe("Loom", () => {
const match = Loom.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://www.loom.com/share/55327cbb265743f39c2c442c029277e0".match(match)
).toBeTruthy();
expect(
"https://www.useloom.com/share/55327cbb265743f39c2c442c029277e0".match(
match
)
).toBeTruthy();
});
test("to be enabled on embed link", () => {
expect(
"https://www.loom.com/embed/55327cbb265743f39c2c442c029277e0".match(match)
).toBeTruthy();
expect(
"https://www.useloom.com/embed/55327cbb265743f39c2c442c029277e0".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://www.useloom.com".match(match)).toBe(null);
expect("https://www.useloom.com/features".match(match)).toBe(null);
expect(
"https://www.loommcom/share/55327cbb265743f39c2c442c029277e0".match(match)
).toBe(null);
});
});

View File

@@ -1,12 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Loom(props: Props) {
const normalizedUrl = props.attrs.href.replace("share", "embed");
return <Frame {...props} src={normalizedUrl} title="Loom Embed" />;
}
Loom.ENABLED = [/^https:\/\/(www\.)?(use)?loom\.com\/(embed|share)\/(.*)$/];
export default Loom;

View File

@@ -1,53 +0,0 @@
import Lucidchart from "./Lucidchart";
describe("Lucidchart", () => {
const match = Lucidchart.ENABLED[0];
test("to be enabled on view link", () => {
expect(
"https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7".match(
match
)
).toBeTruthy();
});
test("to be enabled on root link", () => {
expect(
"https://lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7".match(
match
)
).toBeTruthy();
});
test("to be enabled on app link", () => {
expect(
"https://app.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7".match(
match
)
).toBeTruthy();
});
test("to be enabled on visited link", () => {
expect(
"https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7/0".match(
match
)
).toBeTruthy();
});
test("to be enabled on embedded link", () => {
expect(
"https://app.lucidchart.com/documents/embeddedchart/1af2bdfa-da7d-4ea1-aa1d-bec5677a9837".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://lucidchart.com".match(match)).toBe(null);
expect("https://app.lucidchart.com".match(match)).toBe(null);
expect("https://www.lucidchart.com".match(match)).toBe(null);
expect("https://www.lucidchart.com/features".match(match)).toBe(null);
expect("https://www.lucidchart.com/documents/view".match(match)).toBe(null);
});
});

View File

@@ -1,23 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Lucidchart(props: Props) {
const { matches } = props.attrs;
const chartId = matches.groups?.chartId;
return (
<Frame
{...props}
src={`https://lucidchart.com/documents/embeddedchart/${chartId}`}
title="Lucidchart Embed"
/>
);
}
Lucidchart.ENABLED = [
/^https?:\/\/(www\.|app\.)?(lucidchart\.com|lucid\.app)\/documents\/(embeddedchart|view|edit)\/(?<chartId>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(?:.*)?$/,
/^https?:\/\/(www\.|app\.)?(lucid\.app|lucidchart\.com)\/lucidchart\/(?<chartId>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\/(embeddedchart|view|edit)(?:.*)?$/,
];
export default Lucidchart;

View File

@@ -1,15 +0,0 @@
import Marvel from "./Marvel";
describe("Marvel", () => {
const match = Marvel.ENABLED[0];
test("to be enabled on share link", () => {
expect("https://marvelapp.com/75hj91".match(match)).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://marvelapp.com".match(match)).toBe(null);
expect("https://marvelapp.com/features".match(match)).toBe(null);
expect("https://marvelapppcom/75hj91".match(match)).toBe(null);
});
});

View File

@@ -1,13 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Marvel(props: Props) {
return (
<Frame {...props} src={props.attrs.href} title="Marvel Embed" border />
);
}
Marvel.ENABLED = [new RegExp("^https://marvelapp\\.com/([A-Za-z0-9-]{6})/?$")];
export default Marvel;

View File

@@ -1,58 +0,0 @@
import Mindmeister from "./Mindmeister";
describe("Mindmeister", () => {
const match = Mindmeister.ENABLED[0];
test("to be enabled on mm.tt link", () => {
expect("https://mm.tt/326377934".match(match)).toBeTruthy();
});
test("to be enabled on mm.tt link with token parameter", () => {
expect("https://mm.tt/326377934?t=r9NcnTRr18".match(match)).toBeTruthy();
});
test("to be enabled on embed link", () => {
expect(
"https://www.mindmeister.com/maps/public_map_shell/326377934/paper-digital-or-online-mind-mapping".match(
match
)
).toBeTruthy();
});
test("to be enabled on public link", () => {
expect(
"https://www.mindmeister.com/326377934/paper-digital-or-online-mind-mapping".match(
match
)
).toBeTruthy();
});
test("to be enabled without www", () => {
expect(
"https://mindmeister.com/326377934/paper-digital-or-online-mind-mapping".match(
match
)
).toBeTruthy();
});
test("to be enabled without slug", () => {
expect("https://mindmeister.com/326377934".match(match)).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://mindmeister.com".match(match)).toBe(null);
expect("https://www.mindmeister.com/pricing".match(match)).toBe(null);
expect("https://www.mmttt/326377934".match(match)).toBe(null);
expect(
"https://www.mindmeisterrcom/maps/public_map_shell/326377934/paper-digital-or-online-mind-mapping".match(
match
)
).toBe(null);
expect("https://wwwwmm.tt/326377934".match(match)).toBe(null);
expect(
"https://wwwwmindmeister.com/maps/public_map_shell/326377934/paper-digital-or-online-mind-mapping".match(
match
)
).toBe(null);
});
});

View File

@@ -1,26 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Mindmeister(props: Props) {
const chartId =
props.attrs.matches[4] +
(props.attrs.matches[5] || "") +
(props.attrs.matches[6] || "");
return (
<Frame
{...props}
src={`https://www.mindmeister.com/maps/public_map_shell/${chartId}`}
title="Mindmeister Embed"
border
/>
);
}
Mindmeister.ENABLED = [
new RegExp(
"^https://([w.-]+\\.)?(mindmeister\\.com|mm\\.tt)(/maps/public_map_shell)?/(\\d+)(\\?t=.*)?(/.*)?$"
),
];
export default Mindmeister;

View File

@@ -1,30 +0,0 @@
import Miro from "./Miro";
describe("Miro", () => {
const match = Miro.ENABLED[0];
test("to be enabled on old domain share link", () => {
expect(
"https://realtimeboard.com/app/board/o9J_k0fwiss=".match(match)
).toBeTruthy();
});
test("to be enabled on share link", () => {
expect("https://miro.com/app/board/o9J_k0fwiss=".match(match)).toBeTruthy();
});
test("to extract the domain as part of the match for later use", () => {
expect(
"https://realtimeboard.com/app/board/o9J_k0fwiss=".match(match)?.[1]
).toBe("realtimeboard");
});
test("to not be enabled elsewhere", () => {
expect("https://miro.com".match(match)).toBe(null);
expect("https://realtimeboard.com".match(match)).toBe(null);
expect("https://realtimeboard.com/features".match(match)).toBe(null);
expect(
"https://realtimeboarddcom/app/board/o9J_k0fwiss=".match(match)
).toBe(null);
});
});

View File

@@ -1,24 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function RealtimeBoard(props: Props) {
const { matches } = props.attrs;
const domain = matches[1];
const boardId = matches[2];
const titleName = domain === "realtimeboard" ? "RealtimeBoard" : "Miro";
return (
<Frame
{...props}
src={`https://${domain}.com/app/embed/${boardId}`}
title={`${titleName} (${boardId})`}
/>
);
}
RealtimeBoard.ENABLED = [
/^https:\/\/(realtimeboard|miro)\.com\/app\/board\/(.*)$/,
];
export default RealtimeBoard;

View File

@@ -1,23 +0,0 @@
import ModeAnalytics from "./ModeAnalytics";
describe("ModeAnalytics", () => {
const match = ModeAnalytics.ENABLED[0];
test("to be enabled on report link", () => {
expect(
"https://modeanalytics.com/outline/reports/5aca06064f56".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://modeanalytics.com".match(match)).toBe(null);
expect("https://modeanalytics.com/outline".match(match)).toBe(null);
expect("https://modeanalytics.com/outline/reports".match(match)).toBe(null);
expect(
"https://modeanalyticsscom/outline/reports/5aca06064f56".match(match)
).toBe(null);
expect(
"https://wwwwmodeanalytics.com/outline/reports/5aca06064f56".match(match)
).toBe(null);
});
});

View File

@@ -1,21 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function ModeAnalytics(props: Props) {
// Allow users to paste embed or standard urls and handle them the same
const normalizedUrl = props.attrs.href.replace(/\/embed$/, "");
return (
<Frame
{...props}
src={`${normalizedUrl}/embed`}
title="Mode Analytics Embed"
/>
);
}
ModeAnalytics.ENABLED = [
new RegExp("^https://([w.-]+\\.)?modeanalytics\\.com/(.*)/reports/(.*)$"),
];
export default ModeAnalytics;

View File

@@ -1,9 +0,0 @@
import Otter from "./Otter";
describe("Otter", () => {
const match = Otter.ENABLED[0];
test("to not be enabled for invalid urls", () => {
expect("https://otterrai/s/c9d837d74182317".match(match)).toBe(null);
});
});

View File

@@ -1,19 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Otter(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href}
title="Otter.ai Embed"
referrerPolicy="origin"
border
/>
);
}
Otter.ENABLED = [new RegExp("^https?://otter\\.ai/[su]/(.*)$")];
export default Otter;

View File

@@ -1,18 +0,0 @@
import Pitch from "./Pitch";
describe("Pitch", () => {
const match = Pitch.ENABLED[0];
test("to not be enabled elsewhere", () => {
expect(
"https://appppitch.com/app/presentation/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/player/c9d837d74182317".match(
match
)
).toBe(null);
expect(
"https://app.pitchhcom/app/presentation/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/player/c9d837d74182317".match(
match
)
).toBe(null);
});
});

View File

@@ -1,24 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Pitch(props: Props) {
const shareId = props.attrs.matches[1];
return (
<Frame
{...props}
src={`https://pitch.com/embed/${shareId}`}
title="Pitch Embed"
height="414px"
/>
);
}
Pitch.ENABLED = [
new RegExp(
"^https?://app\\.pitch\\.com/app/(?:presentation/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|public/player)/(.*)$"
),
new RegExp("^https?://pitch\\.com/embed/(.*)$"),
];
export default Pitch;

View File

@@ -1,25 +0,0 @@
import Prezi from "./Prezi";
describe("Prezi", () => {
const match = Prezi.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://prezi.com/view/39mn8Rn1ZkoeEKQCgk5C".match(match)
).toBeTruthy();
});
test("to be enabled on embed link", () => {
expect(
"https://prezi.com/view/39mn8Rn1ZkoeEKQCgk5C/embed".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://prezi.com".match(match)).toBe(null);
expect("https://prezi.com/pricing".match(match)).toBe(null);
expect("https://preziicom/view/39mn8Rn1ZkoeEKQCgk5C".match(match)).toBe(
null
);
});
});

View File

@@ -1,12 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Prezi(props: Props) {
const url = props.attrs.href.replace(/\/embed$/, "");
return <Frame {...props} src={`${url}/embed`} title="Prezi Embed" border />;
}
Prezi.ENABLED = [new RegExp("^https://prezi\\.com/view/(.*)$")];
export default Prezi;

View File

@@ -1,19 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
export default function Scribe(props: Props) {
const { matches } = props.attrs;
const shareId = matches[1];
return (
<Frame
{...props}
src={`https://scribehow.com/embed/${shareId}`}
title="Scribe"
border
/>
);
}
Scribe.ENABLED = [/^https?:\/\/scribehow\.com\/shared\/(.*)$/];

View File

@@ -1,37 +0,0 @@
import Spotify from "./Spotify";
describe("Spotify", () => {
const match = Spotify.ENABLED[0];
test("to be enabled on song link", () => {
expect(
"https://open.spotify.com/track/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg".match(
match
)
).toBeTruthy();
});
test("to be enabled on playlist link", () => {
expect(
"https://open.spotify.com/user/spotify/playlist/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://spotify.com".match(match)).toBe(null);
expect("https://open.spotify.com".match(match)).toBe(null);
expect("https://www.spotify.com".match(match)).toBe(null);
expect(
"https://opennspotify.com/track/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg".match(
match
)
).toBe(null);
expect(
"https://open.spotifyycom/track/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg".match(
match
)
).toBe(null);
});
});

View File

@@ -3,7 +3,7 @@ import styled from "styled-components";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Spotify(props: Props) {
function Spotify({ matches, ...props }: Props) {
let pathname = "";
try {
const parsed = new URL(props.attrs.href);
@@ -39,6 +39,4 @@ const SpotifyFrame = styled(Frame)`
border-radius: 13px;
`;
Spotify.ENABLED = [new RegExp("^https?://open\\.spotify\\.com/(.*)$")];
export default Spotify;

View File

@@ -1,34 +0,0 @@
import Tldraw from "./Tldraw";
describe("Tldraw", () => {
const match = Tldraw.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://beta.tldraw.com/r/v2_c_r5WVtGaktE99D3wyFFsoL".match(match)
).toBeTruthy();
});
test("to be enabled for snapshot link", () => {
expect(
"https://beta.tldraw.com/s/v2_c_r5WVtGaktE99D3wyFFsoL".match(match)
).toBeTruthy();
});
test("to be enabled for read only urls having v", () => {
expect(
"https://www.tldraw.com/v/-2VIEZVwrbtdNq0Pmfpf2?viewport=-608%2C-549%2C1998%2C1943&page=page%3AWd7DfjA-igxOMso931W_S".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://wwww.tldraw.com/r/c9d837d74182317".match(match)).toBe(null);
expect("https://wwwwtldraw.com/r/c9d837d74182317".match(match)).toBe(null);
expect("https://www.tldrawwcom/r/c9d837d74182317".match(match)).toBe(null);
expect(
"https://beta.tldraw.com/e/v2_c_r5WVtGaktE99D3wyFFsoL".match(match)
).toBe(null);
});
});

View File

@@ -1,21 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Tldraw(props: Props) {
return (
<Frame
{...props}
src={props.attrs.href}
title="Tldraw Embed"
referrerPolicy="origin"
border
/>
);
}
Tldraw.ENABLED = [
new RegExp("^https?://(beta|www|old)\\.tldraw\\.com/[rsv]/(.*)"),
];
export default Tldraw;

View File

@@ -1,9 +0,0 @@
import Trello from "./Trello";
describe("Trello", () => {
const match = Trello.ENABLED[0];
test("to not be enabled for invalid urls", () => {
expect("https://trelloocom/c/c9d837d74182317".match(match)).toBe(null);
});
});

View File

@@ -2,8 +2,7 @@ import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Trello(props: Props) {
const { matches } = props.attrs;
function Trello({ matches, ...props }: Props) {
const objectId = matches[2];
if (matches[1] === "c") {
@@ -28,6 +27,4 @@ function Trello(props: Props) {
);
}
Trello.ENABLED = [/^https:\/\/trello\.com\/(c|b)\/([^/]*)(.*)?$/];
export default Trello;

View File

@@ -1,19 +0,0 @@
import Typeform from "./Typeform";
describe("Typeform", () => {
const match = Typeform.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://beardyman.typeform.com/to/zvlr4L".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://www.typeform.com".match(match)).toBe(null);
expect("https://typeform.com/to/zvlr4L".match(match)).toBe(null);
expect("https://typeform.com/features".match(match)).toBe(null);
expect("https://beardymanntypeform.com/to/zvlr4L".match(match)).toBe(null);
expect("https://beardyman.typeformmcom/to/zvlr4L".match(match)).toBe(null);
});
});

View File

@@ -1,15 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Typeform(props: Props) {
return <Frame {...props} src={props.attrs.href} title="Typeform Embed" />;
}
Typeform.ENABLED = [
new RegExp(
"^https://([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)\\.typeform\\.com/to/(.*)$"
),
];
export default Typeform;

View File

@@ -1,19 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
export default function Valtown(props: Props) {
const { matches } = props.attrs;
const valId = matches[1];
return (
<Frame
{...props}
src={`https://www.val.town/embed/${valId}`}
title="Valtown"
border
/>
);
}
Valtown.ENABLED = [/^https?:\/\/(?:www.)?val\.town\/(?:v|embed)\/(.*)$/];

View File

@@ -1,20 +0,0 @@
import Vimeo from "./Vimeo";
describe("Vimeo", () => {
const match = Vimeo.ENABLED[0];
test("to be enabled on video link", () => {
expect("https://vimeo.com/265045525".match(match)).toBeTruthy();
expect("https://vimeo.com/265045525/b9fefc8598".match(match)).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://vimeo.com".match(match)).toBe(null);
expect("https://www.vimeo.com".match(match)).toBe(null);
expect("https://vimeo.com/upgrade".match(match)).toBe(null);
expect("https://vimeo.com/features/video-marketing".match(match)).toBe(
null
);
expect("https://www.vimeoocom/265045525".match(match)).toBe(null);
});
});

View File

@@ -2,8 +2,7 @@ import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Vimeo(props: Props) {
const { matches } = props.attrs;
function Vimeo({ matches, ...props }: Props) {
const videoId = matches[4];
const hId = matches[5];
@@ -21,8 +20,4 @@ function Vimeo(props: Props) {
);
}
Vimeo.ENABLED = [
/(http|https)?:\/\/(www\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|)(\d+)(?:\/|\?)?([\d\w]+)?/,
];
export default Vimeo;

View File

@@ -1,9 +0,0 @@
import Whimsical from "./Whimsical";
describe("Whimsical", () => {
const match = Whimsical.ENABLED[0];
test("to not be enabled for invalid urls", () => {
expect("https://whimsicallcom/a-c9d837d74182317".match(match)).toBe(null);
});
});

View File

@@ -1,22 +0,0 @@
import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function Whimsical(props: Props) {
const { matches } = props.attrs;
const boardId = matches[1];
return (
<Frame
{...props}
src={`https://whimsical.com/embed/${boardId}`}
title="Whimsical"
/>
);
}
Whimsical.ENABLED = [
/^https?:\/\/whimsical\.com\/[0-9a-zA-Z-_~]*-([a-zA-Z0-9]+)\/?$/,
];
export default Whimsical;

View File

@@ -1,41 +0,0 @@
import YouTube from "./YouTube";
describe("YouTube", () => {
const match = YouTube.ENABLED[0];
test("to be enabled on video link", () => {
expect(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ".match(match)
).toBeTruthy();
});
test("to be enabled on video link with timestamp", () => {
expect(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=123s".match(match)
).toBeTruthy();
});
test("to be enabled on embed link", () => {
expect(
"https://www.youtube.com/embed?v=dQw4w9WgXcQ".match(match)
).toBeTruthy();
});
test("to be enabled on shortlink", () => {
expect("https://youtu.be/dQw4w9WgXcQ".match(match)).toBeTruthy();
});
test("to be enabled on shortlink with timestamp", () => {
expect("https://youtu.be/dQw4w9WgXcQ?t=123".match(match)).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://youtu.be".match(match)).toBe(null);
expect("https://youtube.com".match(match)).toBe(null);
expect("https://www.youtube.com".match(match)).toBe(null);
expect("https://www.youtube.com/logout".match(match)).toBe(null);
expect("https://www.youtube.com/feed/subscriptions".match(match)).toBe(
null
);
});
});

View File

@@ -2,8 +2,7 @@ import * as React from "react";
import Frame from "../components/Frame";
import { EmbedProps as Props } from ".";
function YouTube(props: Props) {
const { matches } = props.attrs;
function YouTube({ matches, ...props }: Props) {
const videoId = matches[1];
let start;
@@ -26,8 +25,4 @@ function YouTube(props: Props) {
);
}
YouTube.ENABLED = [
/(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})([\&\?](.*))?$/i,
];
export default YouTube;

View File

@@ -1,67 +1,29 @@
import { EditorState } from "prosemirror-state";
import * as React from "react";
import styled from "styled-components";
import { Primitive } from "utility-types";
import { IntegrationType } from "../../types";
import { IntegrationService, IntegrationType } from "../../types";
import type { IntegrationSettings } from "../../types";
import { urlRegex } from "../../utils/urls";
import Image from "../components/Img";
import Abstract from "./Abstract";
import Airtable from "./Airtable";
import Berrycast from "./Berrycast";
import Bilibili from "./Bilibili";
import Canva from "./Canva";
import Cawemo from "./Cawemo";
import ClickUp from "./ClickUp";
import Codepen from "./Codepen";
import DBDiagram from "./DBDiagram";
import Descript from "./Descript";
import Diagrams from "./Diagrams";
import Figma from "./Figma";
import Framer from "./Framer";
import Gist from "./Gist";
import GitLabSnippet from "./GitLabSnippet";
import Gliffy from "./Gliffy";
import GoogleCalendar from "./GoogleCalendar";
import GoogleDocs from "./GoogleDocs";
import GoogleDrawings from "./GoogleDrawings";
import GoogleDrive from "./GoogleDrive";
import GoogleForms from "./GoogleForms";
import GoogleLookerStudio from "./GoogleLookerStudio";
import GoogleMaps from "./GoogleMaps";
import GoogleSheets from "./GoogleSheets";
import GoogleSlides from "./GoogleSlides";
import Grist from "./Grist";
import InVision from "./InVision";
import Instagram from "./Instagram";
import JSFiddle from "./JSFiddle";
import Linkedin from "./Linkedin";
import Loom from "./Loom";
import Lucidchart from "./Lucidchart";
import Marvel from "./Marvel";
import Mindmeister from "./Mindmeister";
import Miro from "./Miro";
import ModeAnalytics from "./ModeAnalytics";
import Otter from "./Otter";
import Pitch from "./Pitch";
import Prezi from "./Prezi";
import Scribe from "./Scribe";
import Spotify from "./Spotify";
import Tldraw from "./Tldraw";
import Trello from "./Trello";
import Typeform from "./Typeform";
import Valtown from "./Valtown";
import Vimeo from "./Vimeo";
import Whimsical from "./Whimsical";
import YouTube from "./YouTube";
export type EmbedProps = {
isSelected: boolean;
isEditable: boolean;
embed: EmbedDescriptor;
matches: RegExpMatchArray;
attrs: {
href: string;
matches: RegExpMatchArray;
};
};
@@ -75,17 +37,40 @@ const Img = styled(Image)`
`;
export class EmbedDescriptor {
/** An icon that will be used to represent the embed in menus */
icon?: React.ReactNode;
/** The name of the embed. If this embed has a matching integration it should match IntegrationService */
name?: string;
title?: string;
/** The title of the embed */
title: string;
/** A keyboard shortcut that will trigger the embed */
shortcut?: string;
/** Keywords that will match this embed in menus */
keywords?: string;
/** A tooltip that will be shown in menus */
tooltip?: string;
/** Whether the embed should be hidden in menus by default */
defaultHidden?: boolean;
/** A regex that will be used to match the embed when pasting a URL */
regexMatch?: RegExp[];
/**
* A function that will be used to transform the URL. The resulting string is passed as the src
* to the iframe. You can perform any transformations you want here, including changing the domain
*
* If a custom display is needed this function should be left undefined and `component` should be
* used instead.
*/
transformMatch?: (matches: RegExpMatchArray) => string;
/** The node attributes */
attrs?: Record<string, Primitive>;
/** Whether the embed should be visible in menus, always true */
visible?: boolean;
active?: (state: EditorState) => boolean;
component: typeof React.Component | React.FC<any>;
/**
* A React component that will be used to render the embed, if displaying a simple iframe then
* `transformMatch` should be used instead.
*/
component?: React.FunctionComponent<EmbedProps>;
/** The integration settings, if any */
settings?: IntegrationSettings<IntegrationType.Embed>;
constructor(options: Omit<EmbedDescriptor, "matcher">) {
@@ -96,30 +81,23 @@ export class EmbedDescriptor {
this.keywords = options.keywords;
this.tooltip = options.tooltip;
this.defaultHidden = options.defaultHidden;
this.regexMatch = options.regexMatch;
this.transformMatch = options.transformMatch;
this.attrs = options.attrs;
this.visible = options.visible;
this.active = options.active;
this.component = options.component;
this.settings = options.settings;
}
matcher(url: string): boolean | [] | RegExpMatchArray {
const regex = urlRegex(this.settings?.url);
matcher(url: string): false | RegExpMatchArray {
const regexes = this.regexMatch ?? [];
const settingsDomainRegex = urlRegex(this.settings?.url);
// @ts-expect-error not aware of static
const regexes = this.component.ENABLED;
regex &&
regexes.unshift(
new RegExp(
// @ts-expect-error not aware of static
`^${regex.source}${this.component.URL_PATH_REGEX.source}$`
)
);
if (settingsDomainRegex) {
regexes.unshift(settingsDomainRegex);
}
for (const regex of regexes) {
const result = url.match(regex);
if (result) {
return result;
}
@@ -135,18 +113,29 @@ const embeds: EmbedDescriptor[] = [
keywords: "design",
defaultHidden: true,
icon: <Img src="/images/abstract.png" alt="Abstract" />,
component: Abstract,
regexMatch: [
new RegExp("^https?://share\\.(?:go)?abstract\\.com/(.*)$"),
new RegExp("^https?://app\\.(?:go)?abstract\\.com/(?:share|embed)/(.*)$"),
],
transformMatch: (matches: RegExpMatchArray) =>
`https://app.goabstract.com/embed/${matches[1]}`,
}),
new EmbedDescriptor({
title: "Airtable",
keywords: "spreadsheet",
icon: <Img src="/images/airtable.png" alt="Airtable" />,
component: Airtable,
regexMatch: [
new RegExp("^https://airtable.com/(?:embed/)?(app.*/)?(shr.*)$"),
new RegExp("^https://airtable.com/(app.*/)?(pag.*)/form$"),
],
transformMatch: (matches: RegExpMatchArray) =>
`https://airtable.com/embed/${matches[1] ?? ""}${matches[2]}`,
}),
new EmbedDescriptor({
title: "Berrycast",
keywords: "video",
defaultHidden: true,
regexMatch: [/^https:\/\/(www\.)?berrycast.com\/conversations\/(.*)$/i],
icon: <Img src="/images/berrycast.png" alt="Berrycast" />,
component: Berrycast,
}),
@@ -154,155 +143,241 @@ const embeds: EmbedDescriptor[] = [
title: "Bilibili",
keywords: "video",
defaultHidden: true,
regexMatch: [
/(?:https?:\/\/)?(www\.bilibili\.com)\/video\/([\w\d]+)?(\?\S+)?/i,
],
transformMatch: (matches: RegExpMatchArray) =>
`https://player.bilibili.com/player.html?bvid=${matches[2]}&page=1&high_quality=1`,
icon: <Img src="/images/bilibili.png" alt="Bilibili" />,
component: Bilibili,
}),
new EmbedDescriptor({
title: "Canva",
keywords: "design",
regexMatch: [
/^https:\/\/(?:www\.)?canva\.com\/design\/([a-zA-Z0-9]*)\/(.*)$/,
],
transformMatch: (matches: RegExpMatchArray) =>
`https://www.canva.com/design/${matches[1]}/view?embed`,
icon: <Img src="/images/canva.png" alt="Canva" />,
component: Canva,
}),
new EmbedDescriptor({
title: "Cawemo",
keywords: "bpmn process",
defaultHidden: true,
regexMatch: [new RegExp("^https?://cawemo.com/(?:share|embed)/(.*)$")],
transformMatch: (matches: RegExpMatchArray) =>
`https://cawemo.com/embed/${matches[1]}`,
icon: <Img src="/images/cawemo.png" alt="Cawemo" />,
component: Cawemo,
}),
new EmbedDescriptor({
title: "ClickUp",
keywords: "project",
regexMatch: [
new RegExp("^https?://share\\.clickup\\.com/[a-z]/[a-z]/(.*)/(.*)$"),
new RegExp(
"^https?://sharing\\.clickup\\.com/[0-9]+/[a-z]/[a-z]/(.*)/(.*)$"
),
],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/clickup.png" alt="ClickUp" />,
component: ClickUp,
}),
new EmbedDescriptor({
title: "Codepen",
keywords: "code editor",
regexMatch: [new RegExp("^https://codepen.io/(.*?)/(pen|embed)/(.*)$")],
transformMatch: (matches) =>
`https://codepen.io/${matches[1]}/embed/${matches[3]}`,
icon: <Img src="/images/codepen.png" alt="Codepen" />,
component: Codepen,
}),
new EmbedDescriptor({
title: "DBDiagram",
keywords: "diagrams database",
regexMatch: [new RegExp("^https://dbdiagram.io/(embed|d)/(\\w+)$")],
transformMatch: (matches) => `https://dbdiagram.io/embed/${matches[2]}`,
icon: <Img src="/images/dbdiagram.png" alt="DBDiagram" />,
component: DBDiagram,
}),
new EmbedDescriptor({
title: "Diagrams.net",
name: IntegrationService.Diagrams,
keywords: "diagrams drawio",
regexMatch: [/^https:\/\/viewer\.diagrams\.net\/(?!proxy).*(title=\\w+)?/],
icon: <Img src="/images/diagrams.png" alt="Diagrams.net" />,
component: Diagrams,
}),
new EmbedDescriptor({
title: "Descript",
keywords: "audio",
regexMatch: [new RegExp("^https?://share\\.descript\\.com/view/(\\w+)$")],
transformMatch: (matches) =>
`https://share.descript.com/embed/${matches[1]}`,
icon: <Img src="/images/descript.png" alt="Descript" />,
component: Descript,
}),
new EmbedDescriptor({
title: "Figma",
keywords: "design svg vector",
regexMatch: [
new RegExp(
"^https://([w.-]+\\.)?figma\\.com/(file|proto)/([0-9a-zA-Z]{22,128})(?:/.*)?$"
),
],
transformMatch: (matches) =>
`https://www.figma.com/embed?embed_host=outline&url=${encodeURIComponent(
matches[0]
)}`,
icon: <Img src="/images/figma.png" alt="Figma" />,
component: Figma,
}),
new EmbedDescriptor({
title: "Framer",
keywords: "design prototyping",
regexMatch: [new RegExp("^https://framer.cloud/(.*)$")],
transformMatch: (matches) => matches[0],
icon: <Img src="/images/framer.png" alt="Framer" />,
component: Framer,
}),
new EmbedDescriptor({
title: "GitHub Gist",
keywords: "code",
regexMatch: [
new RegExp(
"^https://gist\\.github\\.com/([a-zA-Z\\d](?:[a-zA-Z\\d]|-(?=[a-zA-Z\\d])){0,38})/(.*)$"
),
],
icon: <Img src="/images/github-gist.png" alt="GitHub" />,
component: Gist,
}),
new EmbedDescriptor({
title: "GitLab Snippet",
keywords: "code",
regexMatch: [
new RegExp(`^https://gitlab\\.com/(([a-zA-Z\\d-]+)/)*-/snippets/\\d+$`),
],
icon: <Img src="/images/gitlab.png" alt="GitLab" />,
component: GitLabSnippet,
}),
new EmbedDescriptor({
title: "Gliffy",
keywords: "diagram",
regexMatch: [new RegExp("https?://go\\.gliffy\\.com/go/share/(.*)$")],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/gliffy.png" alt="Gliffy" />,
component: Gliffy,
}),
new EmbedDescriptor({
title: "Diagrams.net",
keywords: "diagrams drawio",
icon: <Img src="/images/diagrams.png" alt="Diagrams.net" />,
component: Diagrams,
}),
new EmbedDescriptor({
title: "Google Maps",
keywords: "maps",
regexMatch: [new RegExp("^https?://www\\.google\\.com/maps/embed\\?(.*)$")],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/google-maps.png" alt="Google Maps" />,
component: GoogleMaps,
visible: true,
}),
new EmbedDescriptor({
title: "Google Drawings",
keywords: "drawings",
transformMatch: (matches: RegExpMatchArray) =>
matches[0].replace("/edit", "/preview"),
regexMatch: [
new RegExp(
"^https://docs\\.google\\.com/drawings/d/(.*)/(edit|preview)(.*)$"
),
],
icon: <Img src="/images/google-drawings.png" alt="Google Drawings" />,
component: GoogleDrawings,
}),
new EmbedDescriptor({
title: "Google Drive",
keywords: "drive",
regexMatch: [new RegExp("^https?://drive\\.google\\.com/file/d/(.*)$")],
transformMatch: (matches) =>
matches[0].replace("/view", "/preview").replace("/edit", "/preview"),
icon: <Img src="/images/google-drive.png" alt="Google Drive" />,
component: GoogleDrive,
}),
new EmbedDescriptor({
title: "Google Docs",
keywords: "documents word",
regexMatch: [new RegExp("^https?://docs\\.google\\.com/document/(.*)$")],
transformMatch: (matches) =>
matches[0].replace("/view", "/preview").replace("/edit", "/preview"),
icon: <Img src="/images/google-docs.png" alt="Google Docs" />,
component: GoogleDocs,
}),
new EmbedDescriptor({
title: "Google Sheets",
keywords: "excel spreadsheet",
regexMatch: [
new RegExp("^https?://docs\\.google\\.com/spreadsheets/d/(.*)$"),
],
transformMatch: (matches) =>
matches[0].replace("/view", "/preview").replace("/edit", "/preview"),
icon: <Img src="/images/google-sheets.png" alt="Google Sheets" />,
component: GoogleSheets,
}),
new EmbedDescriptor({
title: "Google Slides",
keywords: "presentation slideshow",
regexMatch: [
new RegExp("^https?://docs\\.google\\.com/presentation/d/(.*)$"),
],
transformMatch: (matches) =>
matches[0].replace("/edit", "/preview").replace("/pub", "/embed"),
icon: <Img src="/images/google-slides.png" alt="Google Slides" />,
component: GoogleSlides,
}),
new EmbedDescriptor({
title: "Google Calendar",
keywords: "calendar",
regexMatch: [
new RegExp(
"^https?://calendar\\.google\\.com/calendar/embed\\?src=(.*)$"
),
],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/google-calendar.png" alt="Google Calendar" />,
component: GoogleCalendar,
}),
new EmbedDescriptor({
title: "Google Looker Studio",
keywords: "bi business intelligence",
icon: (
<Img src="/images/google-lookerstudio.png" alt="Google Looker Studio" />
),
component: GoogleLookerStudio,
}),
new EmbedDescriptor({
title: "Google Forms",
keywords: "form survey",
regexMatch: [new RegExp("^https?://docs\\.google\\.com/forms/d/(.+)$")],
transformMatch: (matches: RegExpMatchArray) =>
matches[0].replace(
/\/(edit|viewform)(\?.+)?$/,
"/viewform?embedded=true"
),
icon: <Img src="/images/google-forms.png" alt="Google Forms" />,
component: GoogleForms,
}),
new EmbedDescriptor({
title: "Google Looker Studio",
keywords: "bi business intelligence",
regexMatch: [
new RegExp(
"^https?://(lookerstudio|datastudio)\\.google\\.com/(embed|u/0)/reporting/(.*)/page/(.*)(/edit)?$"
),
],
transformMatch: (matches: RegExpMatchArray) =>
matches[0].replace("u/0", "embed").replace("/edit", ""),
icon: (
<Img src="/images/google-lookerstudio.png" alt="Google Looker Studio" />
),
}),
new EmbedDescriptor({
title: "Grist",
name: IntegrationService.Grist,
keywords: "spreadsheet",
regexMatch: [new RegExp("^https?://([a-z.-]+\\.)?getgrist\\.com/(.+)$")],
transformMatch: (matches: RegExpMatchArray) =>
matches[0].replace(/(\?embed=true)?$/, "?embed=true"),
icon: <Img src="/images/grist.png" alt="Grist" />,
component: Grist,
}),
new EmbedDescriptor({
title: "Instagram",
keywords: "post",
regexMatch: [
/^https?:\/\/www\.instagram\.com\/(p|reel)\/([\w-]+)(\/?utm_source=\w+)?/,
],
transformMatch: (matches: RegExpMatchArray) => `${matches[0]}/embed`,
icon: <Img src="/images/instagram.png" alt="Instagram" />,
component: Instagram,
}),
new EmbedDescriptor({
title: "InVision",
keywords: "design prototype",
defaultHidden: true,
regexMatch: [
/^https:\/\/(invis\.io\/.*)|(projects\.invisionapp\.com\/share\/.*)$/,
/^https:\/\/(opal\.invisionapp\.com\/static-signed\/live-embed\/.*)$/,
],
icon: <Img src="/images/invision.png" alt="InVision" />,
component: InVision,
}),
@@ -310,6 +385,7 @@ const embeds: EmbedDescriptor[] = [
title: "JSFiddle",
keywords: "code",
defaultHidden: true,
regexMatch: [new RegExp("^https?://jsfiddle\\.net/(.*)/(.*)$")],
icon: <Img src="/images/jsfiddle.png" alt="JSFiddle" />,
component: JSFiddle,
}),
@@ -317,117 +393,176 @@ const embeds: EmbedDescriptor[] = [
title: "LinkedIn",
keywords: "post",
defaultHidden: true,
regexMatch: [
/^https:\/\/www\.linkedin\.com\/(?:posts\/.*-(ugcPost|activity)-(\d+)-.*|(embed)\/(?:feed\/update\/urn:li:(?:ugcPost|share):(?:\d+)))/,
],
icon: <Img src="/images/linkedin.png" alt="LinkedIn" />,
component: Linkedin,
}),
new EmbedDescriptor({
title: "Loom",
keywords: "video screencast",
regexMatch: [/^https:\/\/(www\.)?(use)?loom\.com\/(embed|share)\/(.*)$/],
transformMatch: (matches: RegExpMatchArray) =>
matches[0].replace("share", "embed"),
icon: <Img src="/images/loom.png" alt="Loom" />,
component: Loom,
}),
new EmbedDescriptor({
title: "Lucidchart",
keywords: "chart",
regexMatch: [
/^https?:\/\/(www\.|app\.)?(lucidchart\.com|lucid\.app)\/documents\/(embeddedchart|view|edit)\/(?<chartId>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(?:.*)?$/,
/^https?:\/\/(www\.|app\.)?(lucid\.app|lucidchart\.com)\/lucidchart\/(?<chartId>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\/(embeddedchart|view|edit)(?:.*)?$/,
],
transformMatch: (matches: RegExpMatchArray) =>
`https://lucidchart.com/documents/embeddedchart/${matches.groups?.chartId}`,
icon: <Img src="/images/lucidchart.png" alt="Lucidchart" />,
component: Lucidchart,
}),
new EmbedDescriptor({
title: "Marvel",
keywords: "design prototype",
regexMatch: [new RegExp("^https://marvelapp\\.com/([A-Za-z0-9-]{6})/?$")],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/marvel.png" alt="Marvel" />,
component: Marvel,
}),
new EmbedDescriptor({
title: "Mindmeister",
keywords: "mindmap",
regexMatch: [
new RegExp(
"^https://([w.-]+\\.)?(mindmeister\\.com|mm\\.tt)(/maps/public_map_shell)?/(\\d+)(\\?t=.*)?(/.*)?$"
),
],
transformMatch: (matches: RegExpMatchArray) => {
const chartId = matches[4] + (matches[5] || "") + (matches[6] || "");
return `https://www.mindmeister.com/maps/public_map_shell/${chartId}`;
},
icon: <Img src="/images/mindmeister.png" alt="Mindmeister" />,
component: Mindmeister,
}),
new EmbedDescriptor({
title: "Miro",
keywords: "whiteboard",
regexMatch: [/^https:\/\/(realtimeboard|miro)\.com\/app\/board\/(.*)$/],
transformMatch: (matches: RegExpMatchArray) =>
`https://${matches[1]}.com/app/embed/${matches[2]}`,
icon: <Img src="/images/miro.png" alt="Miro" />,
component: Miro,
}),
new EmbedDescriptor({
title: "Mode",
keywords: "analytics",
defaultHidden: true,
regexMatch: [
new RegExp("^https://([w.-]+\\.)?modeanalytics\\.com/(.*)/reports/(.*)$"),
],
transformMatch: (matches: RegExpMatchArray) =>
`${matches[0].replace(/\/embed$/, "")}/embed`,
icon: <Img src="/images/mode-analytics.png" alt="Mode" />,
component: ModeAnalytics,
}),
new EmbedDescriptor({
title: "Otter.ai",
keywords: "audio transcription meeting notes",
defaultHidden: true,
regexMatch: [new RegExp("^https?://otter\\.ai/[su]/(.*)$")],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/otter.png" alt="Otter.ai" />,
component: Otter,
}),
new EmbedDescriptor({
title: "Pitch",
keywords: "presentation",
defaultHidden: true,
regexMatch: [
new RegExp(
"^https?://app\\.pitch\\.com/app/(?:presentation/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|public/player)/(.*)$"
),
new RegExp("^https?://pitch\\.com/embed/(.*)$"),
],
transformMatch: (matches: RegExpMatchArray) =>
`https://pitch.com/embed/${matches[1]}`,
icon: <Img src="/images/pitch.png" alt="Pitch" />,
component: Pitch,
}),
new EmbedDescriptor({
title: "Prezi",
keywords: "presentation",
regexMatch: [new RegExp("^https://prezi\\.com/view/(.*)$")],
transformMatch: (matches: RegExpMatchArray) =>
`${matches[0].replace(/\/embed$/, "")}/embed`,
icon: <Img src="/images/prezi.png" alt="Prezi" />,
component: Prezi,
}),
new EmbedDescriptor({
title: "Scribe",
keywords: "screencast",
regexMatch: [/^https?:\/\/scribehow\.com\/shared\/(.*)$/],
transformMatch: (matches: RegExpMatchArray) =>
`https://scribehow.com/embed/${matches[1]}`,
icon: <Img src="/images/scribe.png" alt="Scribe" />,
component: Scribe,
}),
new EmbedDescriptor({
title: "Spotify",
keywords: "music",
regexMatch: [new RegExp("^https?://open\\.spotify\\.com/(.*)$")],
icon: <Img src="/images/spotify.png" alt="Spotify" />,
component: Spotify,
}),
new EmbedDescriptor({
title: "Tldraw",
keywords: "draw schematics diagrams",
regexMatch: [
new RegExp("^https?://(beta|www|old)\\.tldraw\\.com/[rsv]/(.*)"),
],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/tldraw.png" alt="Tldraw" />,
component: Tldraw,
}),
new EmbedDescriptor({
title: "Trello",
keywords: "kanban",
regexMatch: [/^https:\/\/trello\.com\/(c|b)\/([^/]*)(.*)?$/],
icon: <Img src="/images/trello.png" alt="Trello" />,
component: Trello,
}),
new EmbedDescriptor({
title: "Typeform",
keywords: "form survey",
regexMatch: [
new RegExp(
"^https://([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)\\.typeform\\.com/to/(.*)$"
),
],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/typeform.png" alt="Typeform" />,
component: Typeform,
}),
new EmbedDescriptor({
title: "Valtown",
keywords: "code",
regexMatch: [/^https?:\/\/(?:www.)?val\.town\/(?:v|embed)\/(.*)$/],
transformMatch: (matches: RegExpMatchArray) =>
`https://www.val.town/embed/${matches[1]}`,
icon: <Img src="/images/valtown.png" alt="Valtown" />,
component: Valtown,
}),
new EmbedDescriptor({
title: "Vimeo",
keywords: "video",
regexMatch: [
/(http|https)?:\/\/(www\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|)(\d+)(?:\/|\?)?([\d\w]+)?/,
],
icon: <Img src="/images/vimeo.png" alt="Vimeo" />,
component: Vimeo,
}),
new EmbedDescriptor({
title: "Whimsical",
keywords: "whiteboard",
regexMatch: [
/^https?:\/\/whimsical\.com\/[0-9a-zA-Z-_~]*-([a-zA-Z0-9]+)\/?$/,
],
transformMatch: (matches: RegExpMatchArray) =>
`https://whimsical.com/embed/${matches[1]}`,
icon: <Img src="/images/whimsical.png" alt="Whimsical" />,
component: Whimsical,
}),
new EmbedDescriptor({
title: "YouTube",
keywords: "google video",
regexMatch: [
/(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})([\&\?](.*))?$/i,
],
icon: <Img src="/images/youtube.png" alt="YouTube" />,
component: YouTube,
}),

View File

@@ -0,0 +1,15 @@
import { EmbedDescriptor } from "../embeds";
export function getMatchingEmbed(
embeds: EmbedDescriptor[],
href: string
): { embed: EmbedDescriptor; matches: RegExpMatchArray } | undefined {
for (const e of embeds) {
const matches = e.matcher(href);
if (matches) {
return { embed: e, matches };
}
}
return undefined;
}

View File

@@ -1,10 +1,11 @@
import { EmbedDescriptor } from "../embeds";
import filterExcessSeparators from "./filterExcessSeparators";
const embedDescriptor = {
const embedDescriptor = new EmbedDescriptor({
title: "Test",
icon: () => null,
matcher: () => true,
component: () => null,
};
});
describe("filterExcessSeparators", () => {
test("filter hanging end separators", () => {

Some files were not shown because too many files have changed in this diff Show More