Files
outline/shared/editor/embeds/index.tsx
T
Tom Moor adbffc0734 chore: clear mechanical lint warnings (Phase 1) (#12198)
* chore: clear mechanical lint warnings

Drops 44 oxlint warnings (559 → 515) by fixing easy mechanical rules
across the codebase: no-useless-escape, no-duplicate-type-constituents,
no-redundant-type-constituents, no-unused-expressions,
no-meaningless-void-operator, require-array-sort-compare, await-thenable.

* chore: drop callback parameter from useCallback deps

The `open` argument is a parameter of the callback, not a closed-over
variable, so it doesn't belong in the deps array.

* chore: promote cleared lint rules to errors

Promotes the rules cleared in this PR from warn to error so future
violations fail the lint:

- no-unused-expressions
- typescript/await-thenable
- typescript/no-duplicate-type-constituents
- typescript/no-meaningless-void-operator
- typescript/require-array-sort-compare

Removes the override that suppressed no-useless-escape on source
files (the global rule is already error) and fixes the 21 escape
violations that this exposed in regex character classes and template
literals.

* chore: address PR review feedback

- usePinnedDocuments: simplify UrlId to plain string instead of the
  intersection trick.
- PlantUML embed: move - to end of character class so it's a literal
  hyphen rather than a range operator.
- checkboxes: type token params as Token | undefined to match the
  actual call sites that pass tokens[index - 2] etc.
2026-04-28 20:00:03 -04:00

751 lines
25 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 { BrowserIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import type { Primitive } from "utility-types";
import env from "../../env";
import { IntegrationService } from "../../types";
import type { IntegrationSettings, IntegrationType } from "../../types";
import { urlRegex } from "../../utils/urls";
import Image from "../components/Img";
import Berrycast from "./Berrycast";
import Diagrams from "./Diagrams";
import Dropbox from "./Dropbox";
import Gist from "./Gist";
import GitLabSnippet from "./GitLabSnippet";
import InVision from "./InVision";
import JSFiddle from "./JSFiddle";
import Linkedin from "./Linkedin";
import Pinterest from "./Pinterest";
import Spotify from "./Spotify";
import Trello from "./Trello";
import Vimeo from "./Vimeo";
import YouTube from "./YouTube";
import PlantUmlDiagrams from "./PlantUml";
export type EmbedProps = {
isSelected: boolean;
isEditable: boolean;
embed: EmbedDescriptor;
matches: RegExpMatchArray;
attrs: {
href: string;
};
};
const Img = styled(Image)<{ $invertable?: boolean }>`
border-radius: 3px;
margin: 3px;
width: 18px;
height: 18px;
${(props) =>
props.$invertable &&
props.theme.isDark &&
`
filter: invert(1);
`}
`;
export class EmbedDescriptor {
/** A unique identifier for the embed */
id: string;
/** 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 placeholder that will be shown in the URL input */
placeholder?: 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;
/** Whether the embed should match automatically when pasting a URL (default to true) */
matchOnInput?: boolean;
/** A regex that will be used to match the embed from 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>;
/** Whether this embed has been disabled by the team admin */
disabled?: boolean;
constructor(options: Omit<EmbedDescriptor, "matcher">) {
this.id = options.id;
this.icon = options.icon;
this.name = options.name;
this.title = options.title;
this.placeholder = options.placeholder;
this.shortcut = options.shortcut;
this.keywords = options.keywords;
this.tooltip = options.tooltip;
this.defaultHidden = options.defaultHidden;
this.hideToolbar = options.hideToolbar;
this.matchOnInput = options.matchOnInput ?? true;
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({
id: "airtable",
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({
id: "berrycast",
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({
id: "bilibili",
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&autoplay=0`,
icon: <Img src="/images/bilibili.png" alt="Bilibili" />,
}),
new EmbedDescriptor({
id: "camunda",
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({
id: "canva",
title: "Canva",
keywords: "design",
regexMatch: [/^https:\/\/(?:www\.)?canva\.com\/design\/([/a-zA-Z0-9_-]*)$/],
transformMatch: (matches: RegExpMatchArray) => {
const input = matches.input ?? matches[0];
try {
const url = new URL(input);
const params = new URLSearchParams(url.search);
params.append("embed", "");
return `${url.origin}${url.pathname}?${params.toString()}`;
} catch (_err) {
// Ignore
}
return input;
},
icon: <Img src="/images/canva.png" alt="Canva" />,
}),
new EmbedDescriptor({
id: "cawemo",
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({
id: "clickup",
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({
id: "codepen",
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" $invertable />,
}),
new EmbedDescriptor({
id: "dbdiagram",
title: "DBDiagram",
keywords: "diagrams database",
regexMatch: [new RegExp("^https://dbdiagram.io/(embed|e|d)/(\\w+)(/.*)?$")],
transformMatch: (matches) => `https://dbdiagram.io/embed/${matches[2]}`,
icon: <Img src="/images/dbdiagram.png" alt="DBDiagram" />,
}),
new EmbedDescriptor({
id: "diagrams",
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,
visible: false,
}),
new EmbedDescriptor({
id: "descript",
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" />,
}),
...(env.DROPBOX_APP_KEY
? [
new EmbedDescriptor({
id: "dropbox",
title: "Dropbox",
keywords: "file document",
regexMatch: [
new RegExp("^https?://(www.)?dropbox.com/(s|scl)/(.*)$"),
],
icon: <Img src="/images/dropbox.png" alt="Dropbox" />,
component: Dropbox,
}),
]
: []),
new EmbedDescriptor({
id: "figma",
title: "Figma",
keywords: "design svg vector",
regexMatch: [
new RegExp(
"^https://([w.-]+\\.)?figma\\.com/(file|proto|board|design)/([0-9a-zA-Z]{22,128})(?:/.*)?$"
),
new RegExp("^https://([w.-]+\\.)?figma\\.com/embed(.*)$"),
],
transformMatch: (matches) => {
if (matches[0].includes("/embed")) {
return matches[0];
}
return `https://www.figma.com/embed?embed_host=outline&url=${encodeURIComponent(
matches[0]
)}`;
},
icon: <Img src="/images/figma.png" alt="Figma" />,
}),
new EmbedDescriptor({
id: "framer",
title: "Framer",
keywords: "design prototyping",
regexMatch: [new RegExp("^https://framer.cloud/(.*)$")],
transformMatch: (matches) => matches[0],
icon: <Img src="/images/framer.png" alt="Framer" $invertable />,
}),
new EmbedDescriptor({
id: "github-gist",
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" $invertable />,
component: Gist,
}),
new EmbedDescriptor({
id: "gitlab-snippet",
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({
id: "gliffy",
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({
id: "google-maps",
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" />,
}),
new EmbedDescriptor({
id: "google-drawings",
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({
id: "google-drive",
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({
id: "google-docs",
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({
id: "google-sheets",
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({
id: "google-slides",
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({
id: "google-calendar",
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({
id: "google-forms",
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({
id: "google-looker-studio",
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({
id: "grist",
title: "Grist",
name: IntegrationService.Grist,
keywords: "spreadsheet",
regexMatch: [new RegExp("^https?://([a-z.-]+\\.)?getgrist\\.com/(.+)$")],
transformMatch: (matches: RegExpMatchArray) => {
const input = matches.input ?? matches[0];
try {
const url = new URL(input);
const params = new URLSearchParams(url.search);
if (params.has("embed") || params.get("style") === "singlePage") {
return input;
}
params.append("embed", "true");
return `${url.origin}${url.pathname}?${params.toString()}`;
} catch (_err) {
// Ignore
}
return input;
},
icon: <Img src="/images/grist.png" alt="Grist" />,
}),
new EmbedDescriptor({
id: "instagram",
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({
id: "invision",
title: "InVision",
keywords: "design prototype",
defaultHidden: true,
visible: false,
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({
id: "jsfiddle",
title: "JSFiddle",
keywords: "code",
defaultHidden: true,
regexMatch: [new RegExp("^https?://jsfiddle\\.net/(.*)/(.*)$")],
icon: <Img src="/images/jsfiddle.png" alt="JSFiddle" $invertable />,
component: JSFiddle,
}),
new EmbedDescriptor({
id: "linkedin",
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({
id: "loom",
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({
id: "lucidchart",
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({
id: "marvel",
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({
id: "mindmeister",
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({
id: "miro",
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({
id: "mode",
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({
id: "otter",
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({
id: "pitch",
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({
id: "prezi",
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({
id: "scribe",
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({
id: "smartsuite",
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({
id: "spotify",
title: "Spotify",
keywords: "music",
regexMatch: [new RegExp("^https?://open\\.spotify\\.com/(.*)$")],
icon: <Img src="/images/spotify.png" alt="Spotify" />,
component: Spotify,
}),
new EmbedDescriptor({
id: "tella",
title: "Tella",
keywords: "video",
regexMatch: [/^https?:\/\/(?:www\.)?tella\.tv\/video\/([^/]+)(?:.*)?$/],
transformMatch: (matches: RegExpMatchArray) =>
`https://www.tella.tv/video/${matches[1]}/embed?b=0&title=1&a=0&loop=0&t=0&muted=0&wt=1`,
icon: <Img src="/images/tella.png" alt="Tella" />,
defaultHidden: true,
hideToolbar: true,
}),
new EmbedDescriptor({
id: "tldraw",
title: "Tldraw",
keywords: "draw schematics diagrams",
regexMatch: [
new RegExp("^https?://(beta|www|old)\\.tldraw\\.com/[rsvopf]+/(.*)"),
],
transformMatch: (matches: RegExpMatchArray) => matches[0],
icon: <Img src="/images/tldraw.png" alt="Tldraw" $invertable />,
}),
new EmbedDescriptor({
id: "trello",
title: "Trello",
keywords: "kanban",
regexMatch: [/^https:\/\/trello\.com\/(c|b)\/([^/]*)(.*)?$/],
icon: <Img src="/images/trello.png" alt="Trello" />,
component: Trello,
}),
new EmbedDescriptor({
id: "typeform",
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" $invertable />,
}),
new EmbedDescriptor({
id: "valtown",
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" $invertable />,
}),
new EmbedDescriptor({
id: "vimeo",
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({
id: "pinterest",
title: "Pinterest",
keywords: "board moodboard pins",
regexMatch: [
// Match board URLs but exclude pins
/^(?:https?:\/\/)?(?:(?:www\.|[a-z]{2}\.)?pinterest\.(?:com|[a-z]{2,3}))\/(?!pin\/)([^/]+)\/([^/]+)\/?$/,
// Match profile URLs but exclude pins
/^(?:https?:\/\/)?(?:(?:www\.|[a-z]{2}\.)?pinterest\.(?:com|[a-z]{2,3}))\/(?!pin\/)([^/]+)\/?$/,
],
icon: <Img src="/images/pinterest.png" alt="Pinterest" />,
component: Pinterest,
}),
new EmbedDescriptor({
id: "whimsical",
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({
id: "youtube",
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,
}),
new EmbedDescriptor({
id: "plant-uml",
title: "Plant UML",
keywords: "plant plantuml uml",
regexMatch: [
/(?:https?:\/\/)?(?:www\.)?editor\.plantuml\.com\/uml\/([a-zA-Z0-9_-]+)([&?].*)?$/i,
],
icon: <Img src="/images/plantuml.png" alt="PlantUml" />,
component: PlantUmlDiagrams,
}),
/* The generic iframe embed should always be the last one */
new EmbedDescriptor({
id: "embed",
title: "Embed",
keywords: "iframe webpage",
placeholder: "Paste a URL to embed",
icon: <BrowserIcon />,
defaultHidden: false,
matchOnInput: false,
regexMatch: [new RegExp("^https?://(.*)$")],
transformMatch: (matches: RegExpMatchArray) => matches[0],
hideToolbar: true,
}),
];
export default embeds;