Files
outline/shared/editor/embeds/index.tsx
2024-02-07 22:37:23 -05:00

602 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from "react";
import styled from "styled-components";
import { Primitive } from "utility-types";
import { IntegrationService, IntegrationType } from "../../types";
import type { IntegrationSettings } from "../../types";
import { urlRegex } from "../../utils/urls";
import Image from "../components/Img";
import Berrycast from "./Berrycast";
import Diagrams from "./Diagrams";
import Gist from "./Gist";
import GitLabSnippet from "./GitLabSnippet";
import InVision from "./InVision";
import JSFiddle from "./JSFiddle";
import Linkedin from "./Linkedin";
import Spotify from "./Spotify";
import Trello from "./Trello";
import Vimeo from "./Vimeo";
import YouTube from "./YouTube";
export type EmbedProps = {
isSelected: boolean;
isEditable: boolean;
embed: EmbedDescriptor;
matches: RegExpMatchArray;
attrs: {
href: string;
};
};
const Img = styled(Image)`
border-radius: 2px;
background: #fff;
box-shadow: 0 0 0 1px #fff;
margin: 3px;
width: 18px;
height: 18px;
`;
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;
/** 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;
/** Whether the bottom toolbar should be hidden use this when the embed itself includes a footer */
hideToolbar?: 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;
/**
* 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">) {
this.icon = options.icon;
this.name = options.name;
this.title = options.title;
this.shortcut = options.shortcut;
this.keywords = options.keywords;
this.tooltip = options.tooltip;
this.defaultHidden = options.defaultHidden;
this.hideToolbar = options.hideToolbar;
this.regexMatch = options.regexMatch;
this.transformMatch = options.transformMatch;
this.attrs = options.attrs;
this.visible = options.visible;
this.component = options.component;
}
matcher(url: string): false | RegExpMatchArray {
const regexes = this.regexMatch ?? [];
const settingsDomainRegex = urlRegex(this.settings?.url);
if (settingsDomainRegex) {
regexes.unshift(settingsDomainRegex);
}
for (const regex of regexes) {
const result = url.match(regex);
if (result) {
return result;
}
}
return false;
}
}
const embeds: EmbedDescriptor[] = [
new EmbedDescriptor({
title: "Abstract",
keywords: "design",
defaultHidden: true,
icon: <Img src="/images/abstract.png" alt="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" />,
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,
}),
new 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" />,
}),
new EmbedDescriptor({
title: "Camunda Modeler",
keywords: "bpmn process cawemo",
defaultHidden: true,
regexMatch: [
new RegExp("^https?://modeler.cloud.camunda.io/(?:share|embed)/(.*)$"),
],
transformMatch: (matches: RegExpMatchArray) =>
`https://modeler.cloud.camunda.io/embed/${matches[1]}`,
icon: <Img src="/images/camunda.png" alt="Camunda" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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) => {
if (matches[0].includes("style=singlePage")) {
return matches[0];
}
return matches[0].replace(/(\?embed=true)?$/, "?embed=true");
},
icon: <Img src="/images/grist.png" alt="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" />,
}),
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,
}),
new EmbedDescriptor({
title: "JSFiddle",
keywords: "code",
defaultHidden: true,
regexMatch: [new RegExp("^https?://jsfiddle\\.net/(.*)/(.*)$")],
icon: <Img src="/images/jsfiddle.png" alt="JSFiddle" />,
component: JSFiddle,
}),
new 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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
new EmbedDescriptor({
title: "SmartSuite",
regexMatch: [
new RegExp("^https?://app\\.smartsuite\\.com/shared/(.*)(?:\\?)?(?:.*)$"),
],
icon: <Img src="/images/smartsuite.png" alt="SmartSuite" />,
defaultHidden: true,
hideToolbar: true,
transformMatch: (matches: RegExpMatchArray) =>
`https://app.smartsuite.com/shared/${matches[1]}?embed=true&header=false&toolbar=true`,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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" />,
}),
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,
}),
];
export default embeds;