Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions client/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,16 @@ interface NodeModule {
accept(path?: string, callback?: () => void): void;
};
}

declare module 'loop-protect' {
const loopProtect: (code: string) => string;
export default loopProtect;
}

declare module 'mime' {
const mime: {
getType: (filename: string) => string | null;
getExtension: (type: string) => string | null;
};
export default mime;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import blobUtil from 'blob-util';
import PropTypes from 'prop-types';
import React, { useRef, useEffect, useMemo } from 'react';
import styled from 'styled-components';
import loopProtect from 'loop-protect';
Expand All @@ -17,46 +16,59 @@ import {
import { getAllScriptOffsets } from '../../utils/consoleUtils';
import { registerFrame } from '../../utils/dispatcher';
import { createBlobUrl } from './filesReducer';
import type { PreviewFile } from './filesReducer';
import resolvePathsForElementsWithAttribute from '../../../server/utils/resolveUtils';

let objectUrls = {};
let objectPaths = {};
interface EmbedFrameProps {
files: PreviewFile[];
isPlaying: boolean;
basePath: string;
gridOutput: boolean;
textOutput: boolean;
}

interface InjectLocalFilesOptions {
basePath: string;
gridOutput: boolean;
textOutput: boolean;
}

let objectUrls: Record<string, string> = {};
let objectPaths: Record<string, string> = {};

const Frame = styled.iframe`
min-height: 100%;
min-width: 100%;
position: absolute;
border-width: 0;
${({ fullView }) =>
fullView &&
`
position: relative;
`}
`;

function resolveCSSLinksInString(content, files) {
function resolveCSSLinksInString(
content: string,
files: PreviewFile[]
): string {
let newContent = content;
let cssFileStrings = content.match(STRING_REGEX);
cssFileStrings = cssFileStrings || [];
const cssFileStrings = content.match(STRING_REGEX) || [];
cssFileStrings.forEach((cssFileString) => {
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = cssFileString.substr(1, cssFileString.length - 2);
const quoteCharacter = cssFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files);
if (resolvedFile) {
if (resolvedFile.url) {
newContent = newContent.replace(
cssFileString,
quoteCharacter + resolvedFile.url + quoteCharacter
);
}
const filePath = cssFileString.slice(1, -1);
const quoteCharacter = cssFileString[0];
const resolvedFile = resolvePathToFile(filePath, files) as
| PreviewFile
| false
| undefined;
if (resolvedFile && resolvedFile.url) {
newContent = newContent.replace(
cssFileString,
quoteCharacter + resolvedFile.url + quoteCharacter
);
}
}
});
return newContent;
}

function jsPreprocess(jsText) {
function jsPreprocess(jsText: string): string {
let newContent = jsText;
// check the code for js errors before sending it to strip comments
// or loops.
Expand All @@ -72,15 +84,17 @@ function jsPreprocess(jsText) {
return newContent;
}

function resolveJSLinksInString(content, files) {
function resolveJSLinksInString(content: string, files: PreviewFile[]): string {
let newContent = content;
let jsFileStrings = content.match(STRING_REGEX);
jsFileStrings = jsFileStrings || [];
const jsFileStrings = content.match(STRING_REGEX) || [];
jsFileStrings.forEach((jsFileString) => {
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = jsFileString.substr(1, jsFileString.length - 2);
const quoteCharacter = jsFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files);
const filePath = jsFileString.slice(1, -1);
const quoteCharacter = jsFileString[0];
const resolvedFile = resolvePathToFile(filePath, files) as
| PreviewFile
| false
| undefined;

if (resolvedFile) {
if (resolvedFile.url) {
Expand All @@ -101,77 +115,72 @@ function resolveJSLinksInString(content, files) {
return jsPreprocess(newContent);
}

function resolveScripts(sketchDoc, files) {
function resolveScripts(sketchDoc: Document, files: PreviewFile[]): void {
const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach((script) => {
if (
script.getAttribute('src') &&
script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null
) {
const resolvedFile = resolvePathToFile(script.getAttribute('src'), files);
const srcAttr = script.getAttribute('src');
if (srcAttr && srcAttr.match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(srcAttr, files) as
| PreviewFile
| false
| undefined;
if (resolvedFile) {
if (resolvedFile.url) {
script.setAttribute('src', resolvedFile.url);
} else {
// in the future, when using y.js, could remake the blob for only the file(s)
// that changed
const blobUrl = createBlobUrl(resolvedFile);
script.setAttribute('src', blobUrl);
const blobPath = blobUrl.split('/').pop();
// objectUrls[blobUrl] = `${resolvedFile.filePath}${
// resolvedFile.filePath.length > 0 ? '/' : ''
// }${resolvedFile.name}`;
objectUrls[blobUrl] = `${resolvedFile.filePath}/${resolvedFile.name}`;
objectPaths[blobPath] = resolvedFile.name;
// script.setAttribute('data-tag', `${startTag}${resolvedFile.name}`);
// script.removeAttribute('src');
// script.innerHTML = resolvedFile.content; // eslint-disable-line
if (blobPath && resolvedFile.filePath) {
objectUrls[
blobUrl
] = `${resolvedFile.filePath}/${resolvedFile.name}`;
objectPaths[blobPath] = resolvedFile.name;
}
}
}
} else if (
!(
script.getAttribute('src') &&
script.getAttribute('src').match(EXTERNAL_LINK_REGEX)
) !== null
) {
} else if (srcAttr && srcAttr.match(EXTERNAL_LINK_REGEX) === null) {
script.setAttribute('crossorigin', '');
script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line
script.innerHTML = resolveJSLinksInString(script.innerHTML, files);
}
});
}

function resolveStyles(sketchDoc, files) {
function resolveStyles(sketchDoc: Document, files: PreviewFile[]): void {
const inlineCSSInHTML = sketchDoc.getElementsByTagName('style');
const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML);
inlineCSSInHTMLArray.forEach((style) => {
style.innerHTML = resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line
style.innerHTML = resolveCSSLinksInString(style.innerHTML, files);
});

const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]');
const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML);
cssLinksInHTMLArray.forEach((css) => {
if (
css.getAttribute('href') &&
css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null
) {
const resolvedFile = resolvePathToFile(css.getAttribute('href'), files);
const hrefAttr = css.getAttribute('href');
if (hrefAttr && hrefAttr.match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(hrefAttr, files) as
| PreviewFile
| false
| undefined;
if (resolvedFile) {
if (resolvedFile.url) {
css.href = resolvedFile.url; // eslint-disable-line
css.href = resolvedFile.url;
} else {
const style = sketchDoc.createElement('style');
style.innerHTML = `\n${resolvedFile.content}`;
sketchDoc.head.appendChild(style);
css.parentElement.removeChild(css);
if (css.parentElement) {
css.parentElement.removeChild(css);
}
}
}
}
});
}

function resolveJSAndCSSLinks(files) {
const newFiles = [];
function resolveJSAndCSSLinks(files: PreviewFile[]): PreviewFile[] {
const newFiles: PreviewFile[] = [];
files.forEach((file) => {
const newFile = { ...file };
if (file.name.match(/.*\.js$/i)) {
Expand All @@ -184,17 +193,20 @@ function resolveJSAndCSSLinks(files) {
return newFiles;
}

function addLoopProtect(sketchDoc) {
function addLoopProtect(sketchDoc: Document): void {
const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach((script) => {
script.innerHTML = jsPreprocess(script.innerHTML); // eslint-disable-line
});
}

function injectLocalFiles(files, htmlFile, options) {
function injectLocalFiles(
files: PreviewFile[],
htmlFile: PreviewFile,
options: InjectLocalFilesOptions
): string {
const { basePath, gridOutput, textOutput } = options;
let scriptOffs = [];
objectUrls = {};
objectPaths = {};
const resolvedFiles = resolveJSAndCSSLinks(files);
Expand All @@ -207,7 +219,6 @@ function injectLocalFiles(files, htmlFile, options) {

resolvePathsForElementsWithAttribute('src', sketchDoc, resolvedFiles);
resolvePathsForElementsWithAttribute('href', sketchDoc, resolvedFiles);
// should also include background, data, poster, but these are used way less often

resolveScripts(sketchDoc, resolvedFiles);
resolveStyles(sketchDoc, resolvedFiles);
Expand Down Expand Up @@ -239,7 +250,7 @@ p5.prototype.registerMethod('afterSetup', p5.prototype.ensureAccessibleCanvas);`
sketchDoc.head.appendChild(previewScripts);

const sketchDocString = `<!DOCTYPE HTML>\n${sketchDoc.documentElement.outerHTML}`;
scriptOffs = getAllScriptOffsets(sketchDocString);
const scriptOffs = getAllScriptOffsets(sketchDocString);
const consoleErrorsScript = sketchDoc.createElement('script');
consoleErrorsScript.innerHTML = `
window.offs = ${JSON.stringify(scriptOffs)};
Expand All @@ -253,53 +264,71 @@ p5.prototype.registerMethod('afterSetup', p5.prototype.ensureAccessibleCanvas);`
return `<!DOCTYPE HTML>\n${sketchDoc.documentElement.outerHTML}`;
}

function getHtmlFile(files) {
function getHtmlFile(files: PreviewFile[]): PreviewFile | undefined {
return files.filter((file) => file.name.match(/.*\.html$/i))[0];
}

function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) {
const iframe = useRef();
function EmbedFrame({
files,
isPlaying,
basePath,
gridOutput,
textOutput
}: EmbedFrameProps) {
const iframe = useRef<HTMLIFrameElement>(null);
const htmlFile = useMemo(() => getHtmlFile(files), [files]);
const srcRef = useRef();
const srcRef = useRef<string>();

useEffect(() => {
const unsubscribe = registerFrame(
iframe.current.contentWindow,
window.origin
);
return () => {
unsubscribe();
};
});
const cleanup = () => {};
if (iframe.current) {
const unsubscribe = registerFrame(
iframe.current.contentWindow,
window.origin
);
return () => {
unsubscribe();
};
}
return cleanup;
}, []);

function renderSketch() {
const doc = iframe.current;
if (isPlaying) {
if (isPlaying && htmlFile && doc) {
const htmlDoc = injectLocalFiles(files, htmlFile, {
basePath,
gridOutput,
textOutput
});
const generatedHtmlFile = {
const generatedHtmlFile: PreviewFile = {
id: '',
name: 'index.html',
content: htmlDoc
content: htmlDoc,
children: []
};
const htmlUrl = createBlobUrl(generatedHtmlFile);
const toRevoke = srcRef.current;
srcRef.current = htmlUrl;
// BRO FOR SOME REASON YOU HAVE TO DO THIS TO GET IT TO WORK ON SAFARI
setTimeout(() => {
doc.src = htmlUrl;
if (toRevoke) {
blobUtil.revokeObjectURL(toRevoke);
}
}, 0);
} else {
} else if (doc) {
doc.src = '';
}
}

useEffect(renderSketch, [files, isPlaying]);
useEffect(renderSketch, [
files,
isPlaying,
htmlFile,
basePath,
gridOutput,
textOutput
]);
return (
<Frame
aria-label="Sketch Preview"
Expand All @@ -310,18 +339,4 @@ function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) {
);
}

EmbedFrame.propTypes = {
files: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
})
).isRequired,
isPlaying: PropTypes.bool.isRequired,
basePath: PropTypes.string.isRequired,
gridOutput: PropTypes.bool.isRequired,
textOutput: PropTypes.bool.isRequired
};

export default EmbedFrame;
Loading