diff --git a/README.md b/README.md
index 05e52565..18003086 100644
--- a/README.md
+++ b/README.md
@@ -25,12 +25,12 @@
## The problem
Trying to embed well known services (like [CodePen][codepen],
-[CodeSandbox][codesandbox], [GIPHY][giphy], [Instagram][instagram],
-[Lichess][lichess], [Pinterest][pinterest], [Slides][slides],
-[SoundCloud][soundcloud], [Spotify][spotify], [Streamable][streamable],
-[Twitch][twitch], [Twitter][twitter] or [YouTube][youtube]) into your
-[Gatsby][gatsby] website can be hard, since you have to know how this needs to
-be done for all of these different services.
+[CodeSandbox][codesandbox], [Excalidraw][excalidraw], [GIPHY][giphy],
+[Instagram][instagram], [Lichess][lichess], [Pinterest][pinterest],
+[Slides][slides], [SoundCloud][soundcloud], [Spotify][spotify],
+[Streamable][streamable], [Twitch][twitch], [Twitter][twitter] or
+[YouTube][youtube]) into your [Gatsby][gatsby] website can be hard, since you
+have to know how this needs to be done for all of these different services.
## This solution
@@ -49,6 +49,7 @@ empty lines) and replace it with the proper embed-code.
- [Supported services](#supported-services)
- [CodePen](#codepen)
- [CodeSandbox](#codesandbox)
+ - [Excalidraw](#excalidraw)
- [GIPHY](#giphy)
- [Instagram](#instagram)
- [Lichess](#lichess)
@@ -209,6 +210,57 @@ https://codesandbox.io/s/ynn88nx9x?view=split
+### Excalidraw
+
+#### Usage
+
+```md
+https://excalidraw.com/#json=5882351917727744,hCHWNykp-VOHM8S0cV5psw
+```
+
+
+Result
+
+```html
+
+
+
+```
+
+
+
### GIPHY
#### Usage
@@ -785,6 +837,7 @@ MIT
[codepen]: https://codepen.io
[codesandbox]: https://codesandbox.io
+[excalidraw]: https://excalidraw.com
[embedded-tweet-docs]: https://developer.twitter.com/web/embedded-tweets
[gatsby]: https://github.com/gatsbyjs/gatsby
[gatsby-plugin-instagram-embed]: https://github.com/MichaelDeBoey/gatsby-plugin-instagram-embed
diff --git a/package.json b/package.json
index f7006061..edf710e1 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
"streamable",
"twitch",
"twitter",
- "youtube"
+ "youtube",
+ "excalidraw"
],
"author": "Michaƫl De Boey (https://michaeldeboey.be)",
"license": "MIT",
@@ -50,6 +51,7 @@
"@babel/runtime": "^7.9.6",
"fetch-retry": "^3.1.0",
"node-fetch": "^2.6.0",
+ "puppeteer": "^2.1.1",
"unist-util-visit": "^2.0.2"
},
"devDependencies": {
diff --git a/src/__tests__/transformers/Excalidraw.js b/src/__tests__/transformers/Excalidraw.js
new file mode 100644
index 00000000..c0e9e714
--- /dev/null
+++ b/src/__tests__/transformers/Excalidraw.js
@@ -0,0 +1,73 @@
+import cases from 'jest-in-case';
+
+import plugin from '../..';
+import { getHTML, shouldTransform } from '../../transformers/Excalidraw';
+
+import { cache, getMarkdownASTForFile, parseASTToMarkdown } from '../helpers';
+
+cases(
+ 'url validation',
+ ({ url, valid }) => {
+ expect(shouldTransform(url)).toBe(valid);
+ },
+ {
+ 'non-Excalidraw url': {
+ url: 'https://not-an-excalidraw-url.com',
+ valid: false,
+ },
+ "non-Excalidraw url ending with 'excalidraw.com'": {
+ url: 'https://this-is-not-excalidraw.com',
+ valid: false,
+ },
+ "non-Excalidraw url ending with 'excalidraw.com' having '#json='": {
+ url:
+ 'https://this-is-not-excalidraw.com/#json=5882351917727744,hCHWNykp-VOHM8S0cV5psw',
+ valid: false,
+ },
+ "Excalidraw url without '#json='": {
+ url: 'https://excalidraw.com',
+ valid: false,
+ },
+ 'Excalidraw url': {
+ url:
+ 'https://excalidraw.com/#json=5882351917727744,hCHWNykp-VOHM8S0cV5psw',
+ valid: true,
+ },
+ "Excalidraw url with 'www' subdomain": {
+ url:
+ 'https://www.excalidraw.com/#json=5882351917727744,hCHWNykp-VOHM8S0cV5psw',
+ valid: true,
+ },
+ }
+);
+
+test('Gets the correct Excalidraw svg', async () => {
+ const html = await getHTML(
+ 'https://excalidraw.com/#json=5882351917727744,hCHWNykp-VOHM8S0cV5psw'
+ );
+
+ expect(html).toMatchInlineSnapshot(
+ `""`
+ );
+});
+
+test('Plugin can transform Excalidraw links', async () => {
+ const markdownAST = getMarkdownASTForFile('Excalidraw');
+
+ const processedAST = await plugin({ cache, markdownAST });
+
+ expect(parseASTToMarkdown(processedAST)).toMatchInlineSnapshot(`
+ "
+
+
+
+
+
+
+
+
+
+
+ "
+ `);
+}, 30000);
diff --git a/src/__tests__/transformers/__fixtures__/Excalidraw.md b/src/__tests__/transformers/__fixtures__/Excalidraw.md
new file mode 100644
index 00000000..c7782e31
--- /dev/null
+++ b/src/__tests__/transformers/__fixtures__/Excalidraw.md
@@ -0,0 +1,11 @@
+https://not-an-excalidraw-url.com
+
+https://this-is-not-excalidraw.com
+
+https://this-is-not-excalidraw.com/#json=5882351917727744,hCHWNykp-VOHM8S0cV5psw
+
+https://excalidraw.com
+
+https://excalidraw.com/#json=5882351917727744,hCHWNykp-VOHM8S0cV5psw
+
+https://www.excalidraw.com/#json=5882351917727744,hCHWNykp-VOHM8S0cV5psw
diff --git a/src/transformers/Excalidraw.js b/src/transformers/Excalidraw.js
new file mode 100644
index 00000000..4ab56ee4
--- /dev/null
+++ b/src/transformers/Excalidraw.js
@@ -0,0 +1,45 @@
+import puppeteer from 'puppeteer';
+
+const getImage = async (url) => {
+ const browser = await puppeteer.launch({
+ // disable sandbox in production
+ args: process.env.NODE_ENV === 'production' ? ['--no-sandbox'] : [],
+ });
+
+ const page = await browser.newPage();
+ await page.goto(url);
+ await page.click('[aria-label=Export]');
+ await page.click("[aria-label='Scale 3 x']");
+
+ const frame = await page.mainFrame();
+ const result = await frame.evaluate(
+ () =>
+ new Promise((resolve, reject) => {
+ try {
+ delete window.chooseFileSystemEntries;
+ const reader = new FileReader();
+ reader.addEventListener('loadend', () => resolve(reader.result));
+ reader.addEventListener('error', () => reject(reader.error));
+ URL.createObjectURL = (blob) => reader.readAsText(blob);
+
+ const button = document.querySelector('[aria-label="Export to SVG"]');
+ button.click();
+ } catch (error) {
+ reject(error);
+ }
+ })
+ );
+
+ await browser.close();
+ return result;
+};
+
+export const shouldTransform = (url) =>
+ /^https?:\/\/(www\.)?excalidraw\.com\/#json=/.test(url);
+
+export const getHTML = async (url) => {
+ const svg = await getImage(url);
+ return `${svg
+ .replace('