@excalidraw/excalidraw 0.17.1-f597bd3 → 0.17.1-f59b4f6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -653,7 +653,7 @@ class App extends React.Component {
653
653
  ? src.srcdoc(this.state.theme)
654
654
  : undefined, src: src?.type !== "document" ? src?.link ?? "" : undefined,
655
655
  // https://stackoverflow.com/q/18470015
656
- scrolling: "no", referrerPolicy: "no-referrer-when-downgrade", title: "Excalidraw Embedded Content", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture", allowFullScreen: true, sandbox: "allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads" })) })] }) }, el.id));
656
+ scrolling: "no", referrerPolicy: "no-referrer-when-downgrade", title: "Excalidraw Embedded Content", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture", allowFullScreen: true, sandbox: `${src?.sandbox?.allowSameOrigin ? "allow-same-origin" : ""} allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads` })) })] }) }, el.id));
657
657
  }) }));
658
658
  }
659
659
  getFrameNameDOMId = (frameElement) => {
@@ -2849,7 +2849,7 @@ class App extends React.Component {
2849
2849
  ShapeCache.generateElementShape(element, null)[0];
2850
2850
  const [, , , , cx, cy] = getElementAbsoluteCoords(element, this.scene.getNonDeletedElementsMap());
2851
2851
  return shouldTestInside(element)
2852
- ? getClosedCurveShape(roughShape, [element.x, element.y], element.angle, [cx, cy])
2852
+ ? getClosedCurveShape(element, roughShape, [element.x, element.y], element.angle, [cx, cy])
2853
2853
  : getCurveShape(roughShape, [element.x, element.y], element.angle, [
2854
2854
  cx,
2855
2855
  cy,
@@ -1,3 +1,4 @@
1
+ export declare const sanitizeHTMLAttribute: (html: string) => string;
1
2
  export declare const normalizeLink: (link: string) => string;
2
3
  export declare const isLocalLink: (link: string | null) => boolean;
3
4
  /**
@@ -1,10 +1,13 @@
1
1
  import { sanitizeUrl } from "@braintree/sanitize-url";
2
+ export const sanitizeHTMLAttribute = (html) => {
3
+ return html.replace(/"/g, """);
4
+ };
2
5
  export const normalizeLink = (link) => {
3
6
  link = link.trim();
4
7
  if (!link) {
5
8
  return link;
6
9
  }
7
- return sanitizeUrl(link);
10
+ return sanitizeUrl(sanitizeHTMLAttribute(link));
8
11
  };
9
12
  export const isLocalLink = (link) => {
10
13
  return !!(link?.includes(location.origin) || link?.startsWith("/"));
@@ -5,16 +5,17 @@ import { setCursorForShape } from "../cursor";
5
5
  import { newTextElement } from "./newElement";
6
6
  import { wrapText } from "./textElement";
7
7
  import { isIframeElement } from "./typeChecks";
8
+ import { sanitizeHTMLAttribute } from "../data/url";
8
9
  const embeddedLinkCache = new Map();
9
10
  const RE_YOUTUBE = /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
10
- const RE_VIMEO = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
11
+ const RE_VIMEO = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
11
12
  const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
12
- const RE_GH_GIST = /^https:\/\/gist\.github\.com/;
13
- const RE_GH_GIST_EMBED = /^<script[\s\S]*?\ssrc=["'](https:\/\/gist.github.com\/.*?)\.js["']/i;
13
+ const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/;
14
+ const RE_GH_GIST_EMBED = /^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i;
14
15
  // not anchored to start to allow <blockquote> twitter embeds
15
- const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:twitter|x).com/;
16
- const RE_TWITTER_EMBED = /^<blockquote[\s\S]*?\shref=["'](https:\/\/(?:twitter|x).com\/[^"']*)/i;
17
- const RE_VALTOWN = /^https:\/\/(?:www\.)?val.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
16
+ const RE_TWITTER = /(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/;
17
+ const RE_TWITTER_EMBED = /^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:twitter|x)\.com\/[^"']*)/i;
18
+ const RE_VALTOWN = /^https:\/\/(?:www\.)?val\.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
18
19
  const RE_GENERIC_EMBED = /^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
19
20
  const RE_GIPHY = /giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
20
21
  const ALLOWED_DOMAINS = new Set([
@@ -115,55 +116,36 @@ export const getEmbedLink = (link) => {
115
116
  return { link, intrinsicSize: aspectRatio, type };
116
117
  }
117
118
  if (RE_TWITTER.test(link)) {
118
- // the embed srcdoc still supports twitter.com domain only
119
- link = link.replace(/\bx.com\b/, "twitter.com");
120
- let ret;
121
- // assume embed code
122
- if (/<blockquote/.test(link)) {
123
- const srcDoc = createSrcDoc(link);
124
- ret = {
125
- type: "document",
126
- srcdoc: () => srcDoc,
127
- intrinsicSize: { w: 480, h: 480 },
128
- };
129
- // assume regular tweet url
130
- }
131
- else {
132
- ret = {
133
- type: "document",
134
- srcdoc: (theme) => createSrcDoc(`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`),
135
- intrinsicSize: { w: 480, h: 480 },
136
- };
137
- }
119
+ const postId = link.match(RE_TWITTER)[1];
120
+ // the embed srcdoc still supports twitter.com domain only.
121
+ // Note that we don't attempt to parse the username as it can consist of
122
+ // non-latin1 characters, and the username in the url can be set to anything
123
+ // without affecting the embed.
124
+ const safeURL = sanitizeHTMLAttribute(`https://twitter.com/x/status/${postId}`);
125
+ const ret = {
126
+ type: "document",
127
+ srcdoc: (theme) => createSrcDoc(`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${safeURL}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`),
128
+ intrinsicSize: { w: 480, h: 480 },
129
+ sandbox: { allowSameOrigin: true },
130
+ };
138
131
  embeddedLinkCache.set(originalLink, ret);
139
132
  return ret;
140
133
  }
141
134
  if (RE_GH_GIST.test(link)) {
142
- let ret;
143
- // assume embed code
144
- if (/<script>/.test(link)) {
145
- const srcDoc = createSrcDoc(link);
146
- ret = {
147
- type: "document",
148
- srcdoc: () => srcDoc,
149
- intrinsicSize: { w: 550, h: 720 },
150
- };
151
- // assume regular url
152
- }
153
- else {
154
- ret = {
155
- type: "document",
156
- srcdoc: () => createSrcDoc(`
157
- <script src="${link}.js"></script>
135
+ const [, user, gistId] = link.match(RE_GH_GIST);
136
+ const safeURL = sanitizeHTMLAttribute(`https://gist.github.com/${user}/${gistId}`);
137
+ const ret = {
138
+ type: "document",
139
+ srcdoc: () => createSrcDoc(`
140
+ <script src="${safeURL}.js"></script>
158
141
  <style type="text/css">
159
142
  * { margin: 0px; }
160
143
  table, .gist { height: 100%; }
161
144
  .gist .gist-file { height: calc(100vh - 2px); padding: 0px; display: grid; grid-template-rows: 1fr auto; }
162
145
  </style>
163
146
  `),
164
- intrinsicSize: { w: 550, h: 720 },
165
- };
166
- }
147
+ intrinsicSize: { w: 550, h: 720 },
148
+ };
167
149
  embeddedLinkCache.set(link, ret);
168
150
  return ret;
169
151
  }
@@ -98,6 +98,9 @@ export type IframeData = ({
98
98
  h: number;
99
99
  };
100
100
  error?: Error;
101
+ sandbox?: {
102
+ allowSameOrigin?: boolean;
103
+ };
101
104
  } & ({
102
105
  type: "video" | "generic";
103
106
  link: string;
@@ -1,12 +1,13 @@
1
1
  import { FRAME_STYLE } from "../constants";
2
2
  import { getElementAbsoluteCoords } from "../element";
3
3
  import { elementOverlapsWithFrame, getTargetFrame, isElementInFrame, } from "../frame";
4
- import { isEmbeddableElement, isIframeLikeElement, } from "../element/typeChecks";
4
+ import { isEmbeddableElement, isIframeLikeElement, isTextElement, } from "../element/typeChecks";
5
5
  import { renderElement } from "../renderer/renderElement";
6
6
  import { createPlaceholderEmbeddableLabel } from "../element/embeddable";
7
7
  import { EXTERNAL_LINK_IMG, getLinkHandleFromCoords, } from "../components/hyperlink/helpers";
8
8
  import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
9
9
  import { throttleRAF } from "../utils";
10
+ import { getBoundTextElement } from "../element/textElement";
10
11
  const strokeGrid = (context, gridSize, scrollX, scrollY, zoom, width, height) => {
11
12
  const BOLD_LINE_FREQUENCY = 5;
12
13
  let GridLineColor;
@@ -121,21 +122,31 @@ const _renderStaticScene = ({ canvas, rc, elementsMap, allElementsMap, visibleEl
121
122
  .forEach((element) => {
122
123
  try {
123
124
  const frameId = element.frameId || appState.frameToHighlight?.id;
125
+ if (isTextElement(element) &&
126
+ element.containerId &&
127
+ elementsMap.has(element.containerId)) {
128
+ // will be rendered with the container
129
+ return;
130
+ }
131
+ context.save();
124
132
  if (frameId &&
125
133
  appState.frameRendering.enabled &&
126
134
  appState.frameRendering.clip) {
127
- context.save();
128
135
  const frame = getTargetFrame(element, elementsMap, appState);
129
136
  // TODO do we need to check isElementInFrame here?
130
137
  if (frame && isElementInFrame(element, elementsMap, appState)) {
131
138
  frameClip(frame, context, renderConfig, appState);
132
139
  }
133
140
  renderElement(element, elementsMap, allElementsMap, rc, context, renderConfig, appState);
134
- context.restore();
135
141
  }
136
142
  else {
137
143
  renderElement(element, elementsMap, allElementsMap, rc, context, renderConfig, appState);
138
144
  }
145
+ const boundTextElement = getBoundTextElement(element, allElementsMap);
146
+ if (boundTextElement) {
147
+ renderElement(boundTextElement, elementsMap, allElementsMap, rc, context, renderConfig, appState);
148
+ }
149
+ context.restore();
139
150
  if (!isExporting) {
140
151
  renderLinkIcon(element, context, appState, elementsMap);
141
152
  }
@@ -361,8 +361,18 @@ export const renderSceneToSvg = (elements, elementsMap, rsvg, svgRoot, files, re
361
361
  .filter((el) => !isIframeLikeElement(el))
362
362
  .forEach((element) => {
363
363
  if (!element.isDeleted) {
364
+ if (isTextElement(element) &&
365
+ element.containerId &&
366
+ elementsMap.has(element.containerId)) {
367
+ // will be rendered with the container
368
+ return;
369
+ }
364
370
  try {
365
371
  renderElementToSvg(element, elementsMap, rsvg, svgRoot, files, element.x + renderConfig.offsetX, element.y + renderConfig.offsetY, renderConfig);
372
+ const boundTextElement = getBoundTextElement(element, elementsMap);
373
+ if (boundTextElement) {
374
+ renderElementToSvg(boundTextElement, elementsMap, rsvg, svgRoot, files, boundTextElement.x + renderConfig.offsetX, boundTextElement.y + renderConfig.offsetY, renderConfig);
375
+ }
366
376
  }
367
377
  catch (error) {
368
378
  console.error(error);