import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';
import escapeStringRegexp from 'escape-string-regexp';
import srcDoc from 'srcdoc-polyfill';
import loopProtect from 'loop-protect';
import { getBlobUrl } from '../actions/files';
const startTag = '@fs-';
const MEDIA_FILE_REGEX = /^('|")(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json|txt|csv|svg|obj|mp4|ogg|webm|mov)('|")$/i;
const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm;
function getAllScriptOffsets(htmlFile) {
const offs = [];
let found = true;
let lastInd = 0;
let ind = 0;
let endFilenameInd = 0;
let filename = '';
let lineOffset = 0;
while (found) {
ind = htmlFile.indexOf(startTag, lastInd);
if (ind === -1) {
found = false;
} else {
endFilenameInd = htmlFile.indexOf('.js', ind + startTag.length + 3);
filename = htmlFile.substring(ind + startTag.length, endFilenameInd);
lineOffset = htmlFile.substring(0, ind).split('\n').length;
offs.push([lineOffset, filename]);
lastInd = ind + 1;
}
}
return offs;
}
function hijackConsoleLogsScript() {
const s = ``;
return s;
}
function hijackConsoleErrorsScript(offs) {
const s = ``;
return s;
}
class PreviewFrame extends React.Component {
componentDidMount() {
if (this.props.isPlaying) {
this.renderFrameContents();
}
if (this.props.dispatchConsoleEvent) {
window.addEventListener('message', (msg) => {
this.props.dispatchConsoleEvent(msg);
});
}
}
componentDidUpdate(prevProps) {
// if sketch starts or stops playing, want to rerender
if (this.props.isPlaying !== prevProps.isPlaying) {
this.renderSketch();
return;
}
// if the user explicitly clicks on the play button
if (this.props.isPlaying && this.props.previewIsRefreshing) {
this.renderSketch();
return;
}
// if user switches textoutput preferences
if (this.props.isTextOutputPlaying !== prevProps.isTextOutputPlaying) {
this.renderSketch();
return;
}
if (this.props.textOutput !== prevProps.textOutput) {
this.renderSketch();
return;
}
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
this.renderSketch();
return;
}
// small bug - if autorefresh is on, and the usr changes files
// in the sketch, preview will reload
}
componentWillUnmount() {
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).contentDocument.body);
}
clearPreview() {
const doc = ReactDOM.findDOMNode(this);
doc.srcDoc = '';
}
injectLocalFiles() {
let htmlFile = this.props.htmlFile.content;
let scriptOffs = [];
// have to build the array manually because the spread operator is only
// one level down...
htmlFile = hijackConsoleLogsScript() + htmlFile;
const mediaFiles = this.props.files.filter(file => file.url);
const textFiles = this.props.files.filter(file => file.name.match(/(.+\.json$|.+\.txt$|.+\.csv$)/i) && file.url === undefined);
const jsFiles = [];
this.props.jsFiles.forEach(jsFile => {
const newJSFile = { ...jsFile };
let jsFileStrings = newJSFile.content.match(STRING_REGEX);
jsFileStrings = jsFileStrings || [];
jsFileStrings.forEach(jsFileString => {
if (jsFileString.match(MEDIA_FILE_REGEX)) {
const filePath = jsFileString.substr(1, jsFileString.length - 2);
const filePathArray = filePath.split('/');
const fileName = filePathArray[filePathArray.length - 1];
mediaFiles.forEach(file => {
if (file.name === fileName) {
newJSFile.content = newJSFile.content.replace(filePath, file.url); // eslint-disable-line
}
});
textFiles.forEach(file => {
if (file.name === fileName) {
const blobURL = getBlobUrl(file);
this.props.setBlobUrl(file, blobURL);
newJSFile.content = newJSFile.content.replace(filePath, blobURL);
}
});
}
});
newJSFile.content = loopProtect(newJSFile.content);
jsFiles.push(newJSFile);
});
const cssFiles = [];
this.props.cssFiles.forEach(cssFile => {
const newCSSFile = { ...cssFile };
let cssFileStrings = newCSSFile.content.match(STRING_REGEX);
cssFileStrings = cssFileStrings || [];
cssFileStrings.forEach(cssFileString => {
if (cssFileString.match(MEDIA_FILE_REGEX)) {
const filePath = cssFileString.substr(1, cssFileString.length - 2);
const filePathArray = filePath.split('/');
const fileName = filePathArray[filePathArray.length - 1];
mediaFiles.forEach(file => {
if (file.name === fileName) {
newCSSFile.content = newCSSFile.content.replace(filePath, file.url); // eslint-disable-line
}
});
}
});
cssFiles.push(newCSSFile);
});
jsFiles.forEach(jsFile => {
const fileName = escapeStringRegexp(jsFile.name);
const fileRegex = new RegExp(`([\s\S]*?)<\/script>`, 'gmi');
let replacementString;
if (jsFile.url) {
replacementString = ``;
} else {
replacementString = ``;
}
htmlFile = htmlFile.replace(fileRegex, replacementString);
});
cssFiles.forEach(cssFile => {
const fileName = escapeStringRegexp(cssFile.name);
const fileRegex = new RegExp(``, 'gmi');
let replacementString;
if (cssFile.url) {
replacementString = ``;
} else {
replacementString = ``;
}
htmlFile = htmlFile.replace(fileRegex, replacementString);
});
const htmlHead = htmlFile.match(/(?:)([\s\S]*?)(?:<\/head>)/gmi);
const headRegex = new RegExp('head', 'i');
let htmlHeadContents = htmlHead[0].split(headRegex)[1];
htmlHeadContents = htmlHeadContents.slice(1, htmlHeadContents.length - 2);
htmlHeadContents += '\n';
if (this.props.textOutput || this.props.isTextOutputPlaying) {
htmlHeadContents += '\n';
htmlHeadContents += '\n';
htmlHeadContents += '\n';
htmlHeadContents += '\n';
htmlHeadContents += '';
}
htmlFile = htmlFile.replace(/(?:)([\s\S]*?)(?:<\/head>)/gmi, `\n${htmlHeadContents}\n`);
scriptOffs = getAllScriptOffsets(htmlFile);
htmlFile += hijackConsoleErrorsScript(JSON.stringify(scriptOffs));
return htmlFile;
}
renderSketch() {
const doc = ReactDOM.findDOMNode(this);
if (this.props.isPlaying) {
srcDoc.set(doc, this.injectLocalFiles());
if (this.props.endSketchRefresh) {
this.props.endSketchRefresh();
}
} else {
doc.srcdoc = '';
srcDoc.set(doc, ' ');
}
}
renderFrameContents() {
const doc = ReactDOM.findDOMNode(this).contentDocument;
if (doc.readyState === 'complete') {
this.renderSketch();
} else {
setTimeout(this.renderFrameContents, 0);
}
}
render() {
return (
);
}
}
PreviewFrame.propTypes = {
isPlaying: PropTypes.bool.isRequired,
isTextOutputPlaying: PropTypes.bool.isRequired,
textOutput: PropTypes.bool.isRequired,
content: PropTypes.string,
htmlFile: PropTypes.shape({
content: PropTypes.string.isRequired
}),
jsFiles: PropTypes.array.isRequired,
cssFiles: PropTypes.array.isRequired,
files: PropTypes.array.isRequired,
dispatchConsoleEvent: PropTypes.func,
children: PropTypes.element,
autorefresh: PropTypes.bool.isRequired,
endSketchRefresh: PropTypes.func.isRequired,
previewIsRefreshing: PropTypes.bool.isRequired,
fullView: PropTypes.bool,
setBlobUrl: PropTypes.func.isRequired
};
export default PreviewFrame;