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; /** 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; /** The integration settings, if any */ settings?: IntegrationSettings; constructor(options: Omit) { 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 = this.settings?.url ? urlRegex(this.settings?.url) : undefined; 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: DBDiagram, }), new EmbedDescriptor({ title: "Diagrams.net", name: IntegrationService.Diagrams, keywords: "diagrams drawio", regexMatch: [/^https:\/\/viewer\.diagrams\.net\/(?!proxy).*(title=\\w+)?/], icon: 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: Descript, }), new EmbedDescriptor({ title: "Figma", keywords: "design svg vector", regexMatch: [ new RegExp( "^https://([w.-]+\\.)?figma\\.com/(file|proto|design)/([0-9a-zA-Z]{22,128})(?:/.*)?$" ), ], transformMatch: (matches) => `https://www.figma.com/embed?embed_host=outline&url=${encodeURIComponent( matches[0] )}`, icon: Figma, }), new EmbedDescriptor({ title: "Framer", keywords: "design prototyping", regexMatch: [new RegExp("^https://framer.cloud/(.*)$")], transformMatch: (matches) => matches[0], icon: 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: GitHub, component: Gist, }), new EmbedDescriptor({ title: "GitLab Snippet", keywords: "code", regexMatch: [ new RegExp(`^https://gitlab\\.com/(([a-zA-Z\\d-]+)/)*-/snippets/\\d+$`), ], icon: GitLab, component: GitLabSnippet, }), new EmbedDescriptor({ title: "Gliffy", keywords: "diagram", regexMatch: [new RegExp("https?://go\\.gliffy\\.com/go/share/(.*)$")], transformMatch: (matches: RegExpMatchArray) => matches[0], icon: Gliffy, }), new EmbedDescriptor({ title: "Google Maps", keywords: "maps", regexMatch: [new RegExp("^https?://www\\.google\\.com/maps/embed\\?(.*)$")], transformMatch: (matches: RegExpMatchArray) => matches[0], icon: 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: 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: 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: 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: 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: 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: 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: 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: ( Google Looker Studio ), }), new EmbedDescriptor({ title: "Grist", name: IntegrationService.Grist, keywords: "spreadsheet", regexMatch: [new RegExp("^https?://([a-z.-]+\\.)?getgrist\\.com/(.+)$")], transformMatch: (matches: RegExpMatchArray) => { const input = matches.input ?? matches[0]; if (input.includes("style=singlePage")) { return input; } return input.replace(/(\?embed=true)?$/, "?embed=true"); }, icon: 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: 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: InVision, component: InVision, }), new EmbedDescriptor({ title: "JSFiddle", keywords: "code", defaultHidden: true, regexMatch: [new RegExp("^https?://jsfiddle\\.net/(.*)/(.*)$")], icon: 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: 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: Loom, }), new EmbedDescriptor({ title: "Lucidchart", keywords: "chart", regexMatch: [ /^https?:\/\/(www\.|app\.)?(lucidchart\.com|lucid\.app)\/documents\/(embeddedchart|view|edit)\/(?[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\/(?[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: 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: 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: 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: 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: 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: 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: Pitch, }), new EmbedDescriptor({ title: "Prezi", keywords: "presentation", regexMatch: [new RegExp("^https://prezi\\.com/view/(.*)$")], transformMatch: (matches: RegExpMatchArray) => `${matches[0].replace(/\/embed$/, "")}/embed`, icon: Prezi, }), new EmbedDescriptor({ title: "Scribe", keywords: "screencast", regexMatch: [/^https?:\/\/scribehow\.com\/shared\/(.*)$/], transformMatch: (matches: RegExpMatchArray) => `https://scribehow.com/embed/${matches[1]}`, icon: Scribe, }), new EmbedDescriptor({ title: "SmartSuite", regexMatch: [ new RegExp("^https?://app\\.smartsuite\\.com/shared/(.*)(?:\\?)?(?:.*)$"), ], icon: 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: 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: Tldraw, }), new EmbedDescriptor({ title: "Trello", keywords: "kanban", regexMatch: [/^https:\/\/trello\.com\/(c|b)\/([^/]*)(.*)?$/], icon: 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: 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: Valtown, }), new EmbedDescriptor({ title: "Vimeo", keywords: "video", regexMatch: [ /(http|https)?:\/\/(www\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|)(\d+)(?:\/|\?)?([\d\w]+)?/, ], icon: 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: 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: YouTube, component: YouTube, }), ]; export default embeds;