cool to share some of this code between client and server
Squashed commit of the following: commit fb5e82cea930b011792983c7d1cc9f6ecacc7dd4 Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Wed Nov 16 12:28:10 2016 -0500 add server side rendering, untested commit 5c60fb30c46ea49a8d9a0ecb56f39ec778464a8b Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Tue Nov 15 18:26:06 2016 -0500 add redux-form bandage post react update, should probably update to redux-form 6 at some point commit 057b5871e7137179abc93f7821a9690f0ea52c92 Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Tue Nov 15 16:30:09 2016 -0500 remove passing jsFiles and cssFiles to PreviewFrame, fix rendering bug commit 88c56fd36d3a8d88902c79642171988ce37825f2 Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Tue Nov 15 16:21:59 2016 -0500 code cleanup, untested commit 82e5dcf8bca461892f1daf06d38f1eaebe72983f Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Tue Nov 15 15:53:50 2016 -0500 update react and react router, fix a few bugs in rendering code, add ability to parse inline js and css commit e02f4b67803ea45328eff4e53659222f3149964c Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Tue Nov 15 14:43:38 2016 -0500 add almost full code to create preview html correctly, untested commit 12f61b2a1aed4607fab24d01572b647ca6210262 Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Wed Nov 2 17:09:26 2016 -0400 refactor some of the preview html generation code commit 111825846703d5c8959cb18795a3aadb7ebe505c Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Wed Nov 2 11:06:36 2016 -0400 add comments as plan of action commit 1cc2cf5203674732b4057382f1937de38b687078 Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Thu Oct 27 19:34:55 2016 -0400 add href parsing commit e67189298cda9b70645f454ecd541a363980f0e4 Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Thu Oct 27 10:48:36 2016 -0400 continue parsing html commit 1458fb940a15a3dc5d74890211a3073e920b84b8 Author: Cassie Tarakajian <ctarakajian@gmail.com> Date: Wed Oct 26 17:40:31 2016 -0400 start to add html parsing
This commit is contained in:
parent
ced885d03f
commit
04922522cc
15 changed files with 384 additions and 245 deletions
|
@ -1,4 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||
|
||||
function LoginForm(props) {
|
||||
const { fields: { email, password }, handleSubmit, submitting, pristine } = props;
|
||||
|
@ -10,7 +11,7 @@ function LoginForm(props) {
|
|||
aria-label="email"
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
{...email}
|
||||
{...domOnlyProps(email)}
|
||||
/>
|
||||
{email.touched && email.error && <span className="form-error">{email.error}</span>}
|
||||
</p>
|
||||
|
@ -20,7 +21,7 @@ function LoginForm(props) {
|
|||
aria-label="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...password}
|
||||
{...domOnlyProps(password)}
|
||||
/>
|
||||
{password.touched && password.error && <span className="form-error">{password.error}</span>}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||
|
||||
class NewFileForm extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -21,7 +22,7 @@ class NewFileForm extends React.Component {
|
|||
type="text"
|
||||
placeholder="Name"
|
||||
ref="fileName"
|
||||
{...name}
|
||||
{...domOnlyProps(name)}
|
||||
/>
|
||||
<input type="submit" value="Add File" aria-label="add file" />
|
||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||
|
||||
class NewFolderForm extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -21,7 +22,7 @@ class NewFolderForm extends React.Component {
|
|||
type="text"
|
||||
placeholder="Name"
|
||||
ref="fileName"
|
||||
{...name}
|
||||
{...domOnlyProps(name)}
|
||||
/>
|
||||
<input type="submit" value="Add Folder" aria-label="add folder" />
|
||||
</form>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||
|
||||
function NewPasswordForm(props) {
|
||||
const { fields: { password, confirmPassword }, handleSubmit, submitting, invalid, pristine } = props;
|
||||
|
@ -10,7 +11,7 @@ function NewPasswordForm(props) {
|
|||
aria-label="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...password}
|
||||
{...domOnlyProps(password)}
|
||||
/>
|
||||
{password.touched && password.error && <span className="form-error">{password.error}</span>}
|
||||
</p>
|
||||
|
@ -20,7 +21,7 @@ function NewPasswordForm(props) {
|
|||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
aria-label="confirm password"
|
||||
{...confirmPassword}
|
||||
{...domOnlyProps(confirmPassword)}
|
||||
/>
|
||||
{confirmPassword.touched && confirmPassword.error && <span className="form-error">{confirmPassword.error}</span>}
|
||||
</p>
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
// import escapeStringRegexp from 'escape-string-regexp';
|
||||
import srcDoc from 'srcdoc-polyfill';
|
||||
|
||||
import loopProtect from 'loop-protect';
|
||||
import { getBlobUrl } from '../actions/files';
|
||||
import { resolvePathToFile } from '../../../../server/utils/filePath';
|
||||
|
||||
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 MEDIA_FILE_REGEX_NO_QUOTES = /^(?!(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;
|
||||
const TEXT_FILE_REGEX = /(.+\.json$|.+\.txt$|.+\.csv$)/i;
|
||||
const NOT_EXTERNAL_LINK_REGEX = /^(?!(http:\/\/|https:\/\/))/;
|
||||
const EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/;
|
||||
|
||||
function getAllScriptOffsets(htmlFile) {
|
||||
const offs = [];
|
||||
|
@ -33,50 +38,8 @@ function getAllScriptOffsets(htmlFile) {
|
|||
return offs;
|
||||
}
|
||||
|
||||
|
||||
function hijackConsoleLogsScript() {
|
||||
const s = `<script>
|
||||
var iframeWindow = window;
|
||||
var originalConsole = iframeWindow.console;
|
||||
iframeWindow.console = {};
|
||||
|
||||
var methods = [
|
||||
'debug', 'clear', 'error', 'info', 'log', 'warn'
|
||||
];
|
||||
|
||||
var consoleBuffer = [];
|
||||
var LOGWAIT = 500;
|
||||
|
||||
methods.forEach( function(method) {
|
||||
iframeWindow.console[method] = function() {
|
||||
originalConsole[method].apply(originalConsole, arguments);
|
||||
|
||||
var args = Array.from(arguments);
|
||||
args = args.map(function(i) {
|
||||
// catch objects
|
||||
return (typeof i === 'string') ? i : JSON.stringify(i);
|
||||
});
|
||||
|
||||
consoleBuffer.push({
|
||||
method: method,
|
||||
arguments: args,
|
||||
source: 'sketch'
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
if (consoleBuffer.length > 0) {
|
||||
window.parent.postMessage(consoleBuffer, '*');
|
||||
consoleBuffer.length = 0;
|
||||
}
|
||||
}, LOGWAIT);
|
||||
</script>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function hijackConsoleErrorsScript(offs) {
|
||||
const s = `<script>
|
||||
const s = `
|
||||
function getScriptOff(line) {
|
||||
var offs = ${offs};
|
||||
var l = 0;
|
||||
|
@ -111,7 +74,7 @@ function hijackConsoleErrorsScript(offs) {
|
|||
}], '*');
|
||||
return false;
|
||||
};
|
||||
</script>`;
|
||||
`;
|
||||
return s;
|
||||
}
|
||||
|
||||
|
@ -172,117 +135,178 @@ class PreviewFrame extends React.Component {
|
|||
}
|
||||
|
||||
injectLocalFiles() {
|
||||
let htmlFile = this.props.htmlFile.content;
|
||||
const htmlFile = this.props.htmlFile.content;
|
||||
let scriptOffs = [];
|
||||
|
||||
// have to build the array manually because the spread operator is only
|
||||
// one level down...
|
||||
const resolvedFiles = this.resolveJSAndCSSLinks(this.props.files);
|
||||
|
||||
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 parser = new DOMParser();
|
||||
const sketchDoc = parser.parseFromString(htmlFile, 'text/html');
|
||||
|
||||
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);
|
||||
});
|
||||
this.resolvePathsForElementsWithAttribute('src', sketchDoc, resolvedFiles);
|
||||
this.resolvePathsForElementsWithAttribute('href', sketchDoc, resolvedFiles);
|
||||
// should also include background, data, poster, but these are used way less often
|
||||
|
||||
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);
|
||||
});
|
||||
this.resolveScripts(sketchDoc, resolvedFiles);
|
||||
this.resolveStyles(sketchDoc, resolvedFiles);
|
||||
|
||||
jsFiles.forEach(jsFile => {
|
||||
const fileName = escapeStringRegexp(jsFile.name);
|
||||
const fileRegex = new RegExp(`<script.*?src=('|")((\.\/)|\/)?${fileName}('|").*?>([\s\S]*?)<\/script>`, 'gmi');
|
||||
let replacementString;
|
||||
if (jsFile.url) {
|
||||
replacementString = `<script data-tag="${startTag}${jsFile.name}" src="${jsFile.url}"></script>`;
|
||||
} else {
|
||||
replacementString = `<script data-tag="${startTag}${jsFile.name}">\n${jsFile.content}\n</script>`;
|
||||
let scriptsToInject = [
|
||||
'/loop-protect.min.js',
|
||||
'/hijackConsole.js'
|
||||
];
|
||||
if (this.props.isTextOutputPlaying) {
|
||||
let interceptorScripts = [];
|
||||
if (this.props.textOutput === 1) {
|
||||
interceptorScripts = [
|
||||
'/interceptor/loadData.js',
|
||||
'/interceptor/intercept-helper-functions.js',
|
||||
'/interceptor/textInterceptor/interceptor-functions.js',
|
||||
'/interceptor/textInterceptor/intercept-p5.js',
|
||||
'/interceptor/ntc.min.js'
|
||||
];
|
||||
} else if (this.props.textOutput === 2) {
|
||||
interceptorScripts = [
|
||||
'/interceptor/loadData.js',
|
||||
'/interceptor/intercept-helper-functions.js',
|
||||
'/interceptor/gridInterceptor/interceptor-functions.js',
|
||||
'/interceptor/gridInterceptor/intercept-p5.js',
|
||||
'/interceptor/ntc.min.js'
|
||||
];
|
||||
} else if (this.props.textOutput === 3) {
|
||||
interceptorScripts = [
|
||||
'/interceptor/loadData.js',
|
||||
'/interceptor/soundInterceptor/intercept-p5.js'
|
||||
];
|
||||
}
|
||||
htmlFile = htmlFile.replace(fileRegex, replacementString);
|
||||
});
|
||||
|
||||
cssFiles.forEach(cssFile => {
|
||||
const fileName = escapeStringRegexp(cssFile.name);
|
||||
const fileRegex = new RegExp(`<link.*?href=('|")((\.\/)|\/)?${fileName}('|").*?>`, 'gmi');
|
||||
let replacementString;
|
||||
if (cssFile.url) {
|
||||
replacementString = `<link rel="stylesheet" href="${cssFile.url}" >`;
|
||||
} else {
|
||||
replacementString = `<style>\n${cssFile.content}\n</style>`;
|
||||
}
|
||||
htmlFile = htmlFile.replace(fileRegex, replacementString);
|
||||
});
|
||||
|
||||
const htmlHead = htmlFile.match(/(?:<head.*?>)([\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 += '<script type="text/javascript" src="/loop-protect.min.js"></script>\n';
|
||||
|
||||
if (this.props.textOutput === 1 || this.props.isTextOutputPlaying) {
|
||||
htmlHeadContents += '<script src="/interceptor/loadData.js"></script>\n';
|
||||
htmlHeadContents += '<script src="/interceptor/intercept-helper-functions.js"></script>\n';
|
||||
htmlHeadContents += '<script src="/interceptor/textInterceptor/interceptor-functions.js"></script>\n';
|
||||
htmlHeadContents += '<script src="/interceptor/textInterceptor/intercept-p5.js"></script>\n';
|
||||
htmlHeadContents += '<script type="text/javascript" src="/interceptor/ntc.min.js"></script>';
|
||||
} else if (this.props.textOutput === 2 || this.props.isTextOutputPlaying) {
|
||||
htmlHeadContents += '<script src="/interceptor/loadData.js"></script>\n';
|
||||
htmlHeadContents += '<script src="/interceptor/intercept-helper-functions.js"></script>\n';
|
||||
htmlHeadContents += '<script src="/interceptor/gridInterceptor/interceptor-functions.js"></script>\n';
|
||||
htmlHeadContents += '<script src="/interceptor/gridInterceptor/intercept-p5.js"></script>\n';
|
||||
htmlHeadContents += '<script type="text/javascript" src="/interceptor/ntc.min.js"></script>';
|
||||
} else if (this.props.textOutput === 3 || this.props.isTextOutputPlaying) {
|
||||
htmlHeadContents += '<script src="/interceptor/loadData.js"></script>\n';
|
||||
htmlHeadContents += '<script src="/interceptor/soundInterceptor/intercept-p5.js"></script>\n';
|
||||
scriptsToInject = scriptsToInject.concat(interceptorScripts);
|
||||
}
|
||||
|
||||
htmlFile = htmlFile.replace(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi, `<head>\n${htmlHeadContents}\n</head>`);
|
||||
scriptsToInject.forEach(scriptToInject => {
|
||||
const script = sketchDoc.createElement('script');
|
||||
script.src = scriptToInject;
|
||||
sketchDoc.head.appendChild(script);
|
||||
});
|
||||
|
||||
scriptOffs = getAllScriptOffsets(htmlFile);
|
||||
htmlFile += hijackConsoleErrorsScript(JSON.stringify(scriptOffs));
|
||||
const sketchDocString = `<!DOCTYPE HTML>\n${sketchDoc.documentElement.outerHTML}`;
|
||||
scriptOffs = getAllScriptOffsets(sketchDocString);
|
||||
const consoleErrorsScript = sketchDoc.createElement('script');
|
||||
consoleErrorsScript.innerHTML = hijackConsoleErrorsScript(JSON.stringify(scriptOffs));
|
||||
sketchDoc.head.appendChild(consoleErrorsScript);
|
||||
|
||||
return htmlFile;
|
||||
return `<!DOCTYPE HTML>\n${sketchDoc.documentElement.outerHTML}`;
|
||||
}
|
||||
|
||||
resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
|
||||
const elements = sketchDoc.querySelectorAll(`[${attr}]`);
|
||||
elements.forEach(element => {
|
||||
if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) {
|
||||
const resolvedFile = resolvePathToFile(element.getAttribute(attr), files);
|
||||
if (resolvedFile) {
|
||||
element.setAttribute(attr, resolvedFile.url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolveJSAndCSSLinks(files) {
|
||||
const newFiles = [];
|
||||
files.forEach(file => {
|
||||
const newFile = { ...file };
|
||||
if (file.name.match(/.*\.js$/i)) {
|
||||
newFile.content = this.resolveJSLinksInString(newFile.content, files);
|
||||
} else if (file.name.match(/.*\.css$/i)) {
|
||||
newFile.content = this.resolveCSSLinksInString(newFile.content, files);
|
||||
}
|
||||
newFiles.push(newFile);
|
||||
});
|
||||
return newFiles;
|
||||
}
|
||||
|
||||
resolveJSLinksInString(content, files) {
|
||||
let newContent = content;
|
||||
let jsFileStrings = content.match(STRING_REGEX);
|
||||
jsFileStrings = jsFileStrings || [];
|
||||
jsFileStrings.forEach(jsFileString => {
|
||||
if (jsFileString.match(MEDIA_FILE_REGEX)) {
|
||||
const filePath = jsFileString.substr(1, jsFileString.length - 2);
|
||||
const resolvedFile = resolvePathToFile(filePath, files);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
newContent = newContent.replace(filePath, resolvedFile.url);
|
||||
} else if (resolvedFile.name.match(TEXT_FILE_REGEX)) {
|
||||
// could also pull file from API instead of using bloburl
|
||||
const blobURL = getBlobUrl(resolvedFile);
|
||||
this.props.setBlobUrl(resolvedFile, blobURL);
|
||||
newContent = newContent.replace(filePath, blobURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
newContent = loopProtect(newContent);
|
||||
return newContent;
|
||||
}
|
||||
|
||||
resolveCSSLinksInString(content, files) {
|
||||
let newContent = content;
|
||||
let cssFileStrings = content.match(STRING_REGEX);
|
||||
cssFileStrings = cssFileStrings || [];
|
||||
cssFileStrings.forEach(cssFileString => {
|
||||
if (cssFileString.match(MEDIA_FILE_REGEX)) {
|
||||
const filePath = cssFileString.substr(1, cssFileString.length - 2);
|
||||
const resolvedFile = resolvePathToFile(filePath, files);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
newContent = newContent.replace(filePath, resolvedFile.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return newContent;
|
||||
}
|
||||
|
||||
resolveScripts(sketchDoc, files) {
|
||||
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);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
script.setAttribute('src', resolvedFile.url);
|
||||
} else {
|
||||
script.removeAttribute('src');
|
||||
script.innerHTML = resolvedFile.content; // eslint-disable-line
|
||||
}
|
||||
}
|
||||
} else if (!(script.getAttribute('src') && script.getAttribute('src').match(EXTERNAL_LINK_REGEX)) !== null) {
|
||||
script.innerHTML = this.resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolveStyles(sketchDoc, files) {
|
||||
const inlineCSSInHTML = sketchDoc.getElementsByTagName('style');
|
||||
const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML);
|
||||
inlineCSSInHTMLArray.forEach(style => {
|
||||
style.innerHTML = this.resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line
|
||||
});
|
||||
|
||||
const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]');
|
||||
cssLinksInHTML.forEach(css => {
|
||||
if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
|
||||
const resolvedFile = resolvePathToFile(css.getAttribute('href'), files);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
css.href = resolvedFile.url; // eslint-disable-line
|
||||
} else {
|
||||
const style = sketchDoc.createElement('style');
|
||||
style.innerHTML = `\n${resolvedFile.content}`;
|
||||
sketchDoc.head.appendChild(style);
|
||||
css.parentElement.removeChild(css);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderSketch() {
|
||||
|
@ -331,8 +355,6 @@ PreviewFrame.propTypes = {
|
|||
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,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||
|
||||
function ResetPasswordForm(props) {
|
||||
const { fields: { email }, handleSubmit, submitting, invalid, pristine } = props;
|
||||
|
@ -10,7 +11,7 @@ function ResetPasswordForm(props) {
|
|||
aria-label="email"
|
||||
type="text"
|
||||
placeholder="Email used for registration"
|
||||
{...email}
|
||||
{...domOnlyProps(email)}
|
||||
/>
|
||||
</p>
|
||||
<input type="submit" disabled={submitting || invalid || pristine || props.user.resetPasswordInitiate} value="Send password reset email" aria-label="Send email to reset password" />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||
|
||||
function SignupForm(props) {
|
||||
const { fields: { username, email, password, confirmPassword }, handleSubmit, submitting, invalid, pristine } = props;
|
||||
|
@ -10,7 +11,7 @@ function SignupForm(props) {
|
|||
aria-label="username"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
{...username}
|
||||
{...domOnlyProps(username)}
|
||||
/>
|
||||
{username.touched && username.error && <span className="form-error">{username.error}</span>}
|
||||
</p>
|
||||
|
@ -20,7 +21,7 @@ function SignupForm(props) {
|
|||
aria-label="email"
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
{...email}
|
||||
{...domOnlyProps(email)}
|
||||
/>
|
||||
{email.touched && email.error && <span className="form-error">{email.error}</span>}
|
||||
</p>
|
||||
|
@ -30,7 +31,7 @@ function SignupForm(props) {
|
|||
aria-label="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...password}
|
||||
{...domOnlyProps(password)}
|
||||
/>
|
||||
{password.touched && password.error && <span className="form-error">{password.error}</span>}
|
||||
</p>
|
||||
|
@ -40,7 +41,7 @@ function SignupForm(props) {
|
|||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
aria-label="confirm password"
|
||||
{...confirmPassword}
|
||||
{...domOnlyProps(confirmPassword)}
|
||||
/>
|
||||
{confirmPassword.touched && confirmPassword.error && <span className="form-error">{confirmPassword.error}</span>}
|
||||
</p>
|
||||
|
|
|
@ -22,7 +22,7 @@ import * as EditorAccessibilityActions from '../actions/editorAccessibility';
|
|||
import * as PreferencesActions from '../actions/preferences';
|
||||
import * as UserActions from '../../User/actions';
|
||||
import * as ToastActions from '../actions/toast';
|
||||
import { getHTMLFile, getJSFiles, getCSSFiles } from '../reducers/files';
|
||||
import { getHTMLFile } from '../reducers/files';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import Overlay from '../../App/components/Overlay';
|
||||
import SketchList from '../components/SketchList';
|
||||
|
@ -344,8 +344,6 @@ class IDEView extends React.Component {
|
|||
</div>
|
||||
<PreviewFrame
|
||||
htmlFile={this.props.htmlFile}
|
||||
jsFiles={this.props.jsFiles}
|
||||
cssFiles={this.props.cssFiles}
|
||||
files={this.props.files}
|
||||
content={this.props.selectedFile.content}
|
||||
isPlaying={this.props.ide.isPlaying}
|
||||
|
@ -559,8 +557,6 @@ IDEView.propTypes = {
|
|||
}),
|
||||
setSelectedFile: PropTypes.func.isRequired,
|
||||
htmlFile: PropTypes.object.isRequired,
|
||||
jsFiles: PropTypes.array.isRequired,
|
||||
cssFiles: PropTypes.array.isRequired,
|
||||
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||
newFile: PropTypes.func.isRequired,
|
||||
closeNewFileModal: PropTypes.func.isRequired,
|
||||
|
@ -617,8 +613,6 @@ function mapStateToProps(state) {
|
|||
files: state.files,
|
||||
selectedFile: state.files.find(file => file.isSelectedFile),
|
||||
htmlFile: getHTMLFile(state.files),
|
||||
jsFiles: getJSFiles(state.files),
|
||||
cssFiles: getCSSFiles(state.files),
|
||||
ide: state.ide,
|
||||
preferences: state.preferences,
|
||||
editorAccessibility: state.editorAccessibility,
|
||||
|
|
16
client/utils/reduxFormUtils.js
Normal file
16
client/utils/reduxFormUtils.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* eslint-disable */
|
||||
export const domOnlyProps = ({
|
||||
initialValue,
|
||||
autofill,
|
||||
onUpdate,
|
||||
valid,
|
||||
invalid,
|
||||
dirty,
|
||||
pristine,
|
||||
active,
|
||||
touched,
|
||||
visited,
|
||||
autofilled,
|
||||
error,
|
||||
...domProps }) => domProps;
|
||||
/* eslint-enable */
|
|
@ -57,7 +57,7 @@
|
|||
"webpack-manifest-plugin": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^1.1.0",
|
||||
|
@ -82,6 +82,7 @@
|
|||
"file-type": "^3.8.0",
|
||||
"htmlhint": "^0.9.13",
|
||||
"js-beautify": "^1.6.4",
|
||||
"jsdom": "^9.8.3",
|
||||
"jshint": "^2.9.2",
|
||||
"lodash": "^4.16.4",
|
||||
"loop-protect": "git+https://git@github.com/catarak/loop-protect.git",
|
||||
|
@ -93,11 +94,11 @@
|
|||
"passport": "^0.3.2",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"react": "^15.0.2",
|
||||
"react-dom": "^15.0.2",
|
||||
"react": "^15.1.0",
|
||||
"react-dom": "^15.1.0",
|
||||
"react-inlinesvg": "^0.4.2",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.4.1",
|
||||
"react-router": "^2.6.0",
|
||||
"react-split-pane": "^0.1.44",
|
||||
"redux": "^3.5.2",
|
||||
"redux-form": "^5.3.3",
|
||||
|
|
|
@ -2,35 +2,12 @@ import Project from '../models/project';
|
|||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
const startTag = '@fs-';
|
||||
import { resolvePathToFile } from '../utils/filePath';
|
||||
|
||||
function injectMediaUrls(filesToInject, allFiles, projectId) {
|
||||
filesToInject.forEach(file => {
|
||||
let fileStrings = file.content.match(/(['"])((\\\1|.)*?)\1/gm);
|
||||
const fileStringRegex = /^('|")(?!(http:\/\/|https:\/\/)).*('|")$/i;
|
||||
fileStrings = fileStrings || [];
|
||||
fileStrings.forEach(fileString => {
|
||||
//if string does not begin with http or https
|
||||
if (fileString.match(fileStringRegex)) {
|
||||
const filePath = fileString.substr(1, fileString.length - 2);
|
||||
const resolvedFile = resolvePathToFile(filePath, allFiles);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
file.content = file.content.replace(filePath,resolvedFile.url);
|
||||
} else if (resolvedFile.name.match(/(.+\.json$|.+\.txt$|.+\.csv$)/i)) {
|
||||
let resolvedFilePath = filePath;
|
||||
if (resolvedFilePath.startsWith('.')) {
|
||||
resolvedFilePath = resolvedFilePath.substr(1);
|
||||
}
|
||||
while (resolvedFilePath.startsWith('/')) {
|
||||
resolvedFilePath = resolvedFilePath.substr(1);
|
||||
}
|
||||
file.content = file.content.replace(filePath, `/api/projects/${projectId}/${resolvedFilePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
import {
|
||||
injectMediaUrls,
|
||||
resolvePathsForElementsWithAttribute,
|
||||
resolveScripts,
|
||||
resolveStyles } from '../utils/previewGeneration';
|
||||
import jsdom, { serializeDocument } from 'jsdom';
|
||||
|
||||
export function serveProject(req, res) {
|
||||
Project.findById(req.params.project_id)
|
||||
|
@ -38,25 +15,17 @@ export function serveProject(req, res) {
|
|||
//TODO this does not parse html
|
||||
const files = project.files;
|
||||
let htmlFile = files.find(file => file.name.match(/\.html$/i)).content;
|
||||
const jsFiles = files.filter(file => file.name.match(/\.js$/i));
|
||||
const cssFiles = files.filter(file => file.name.match(/\.css$/i));
|
||||
const filesToInject = files.filter(file => file.name.match(/\.(js|css)$/i));
|
||||
injectMediaUrls(filesToInject, files, req.params.project_id);
|
||||
|
||||
injectMediaUrls(jsFiles, files, req.params.project_id);
|
||||
injectMediaUrls(cssFiles, files, req.params.project_id);
|
||||
jsdom.env(htmlFile, (err, window) => {
|
||||
const sketchDoc = window.document;
|
||||
resolvePathsForElementsWithAttribute('src', sketchDoc, files);
|
||||
resolvePathsForElementsWithAttribute('href', sketchDoc, files);
|
||||
resolveScripts(sketchDoc, files);
|
||||
resolveStyles(sketchDoc, files);
|
||||
|
||||
jsFiles.forEach(jsFile => {
|
||||
const fileName = escapeStringRegexp(jsFile.name);
|
||||
const fileRegex = new RegExp(`<script.*?src=('|")((\.\/)|\/)?${fileName}('|").*?>([\s\S]*?)<\/script>`, 'gmi');
|
||||
const replacementString = `<script data-tag="${startTag}${jsFile.name}">\n${jsFile.content}\n</script>`;
|
||||
htmlFile = htmlFile.replace(fileRegex, replacementString);
|
||||
res.send(serializeDocument(sketchDoc));
|
||||
});
|
||||
|
||||
cssFiles.forEach(cssFile => {
|
||||
const fileName = escapeStringRegexp(cssFile.name);
|
||||
const fileRegex = new RegExp(`<link.*?href=('|")((\.\/)|\/)?${fileName}('|").*?>`, 'gmi');
|
||||
htmlFile = htmlFile.replace(fileRegex, `<style>\n${cssFile.content}\n</style>`);
|
||||
});
|
||||
|
||||
res.send(htmlFile);
|
||||
});
|
||||
}
|
|
@ -1,38 +1,32 @@
|
|||
export function resolvePathToFile(filePath, files) {
|
||||
const filePathArray = filePath.split('/');
|
||||
let resolvedFile;
|
||||
let currentFile;
|
||||
let currentFile = files.find(file => file.name === 'root');
|
||||
filePathArray.some((filePathSegment, index) => {
|
||||
if (filePathSegment === "" || filePathSegment === ".") {
|
||||
if (filePathSegment === '' || filePathSegment === '.') {
|
||||
return false;
|
||||
} else if (filePathSegment === "..") {
|
||||
} else if (filePathSegment === '..') {
|
||||
return true;
|
||||
} else {
|
||||
if (!currentFile) {
|
||||
const file = files.find(file => file.name === filePathSegment);
|
||||
if (!file) {
|
||||
return true;
|
||||
}
|
||||
currentFile = file;
|
||||
if (index === filePathArray.length - 1) {
|
||||
resolvedFile = file;
|
||||
}
|
||||
} else {
|
||||
const childFiles = currentFile.children.map(childFileId => {
|
||||
return files.find(file => {
|
||||
return file._id.valueOf().toString() === childFileId.valueOf();
|
||||
});
|
||||
});
|
||||
childFiles.some(childFile => {
|
||||
if (childFile.name === filePathSegment) {
|
||||
currentFile = childFile;
|
||||
if (index === filePathArray.length - 1) {
|
||||
resolvedFile = childFile;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let foundChild = false;
|
||||
const childFiles = currentFile.children.map(childFileId =>
|
||||
files.find(file =>
|
||||
file._id.valueOf().toString() === childFileId.valueOf()
|
||||
)
|
||||
);
|
||||
childFiles.some(childFile => {
|
||||
if (childFile.name === filePathSegment) {
|
||||
currentFile = childFile;
|
||||
foundChild = true;
|
||||
if (index === filePathArray.length - 1) {
|
||||
resolvedFile = childFile;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return !foundChild;
|
||||
});
|
||||
return resolvedFile;
|
||||
}
|
||||
}
|
||||
|
|
102
server/utils/previewGeneration.js
Normal file
102
server/utils/previewGeneration.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { resolvePathToFile } from '../utils/filePath';
|
||||
|
||||
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 MEDIA_FILE_REGEX_NO_QUOTES = /^(?!(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;
|
||||
const TEXT_FILE_REGEX = /(.+\.json$|.+\.txt$|.+\.csv$)/i;
|
||||
const EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/;
|
||||
const NOT_EXTERNAL_LINK_REGEX = /^(?!(http:\/\/|https:\/\/))/;
|
||||
|
||||
function resolveLinksInString(content, files, projectId) {
|
||||
let newContent = content;
|
||||
let fileStrings = content.match(STRING_REGEX);
|
||||
const fileStringRegex = /^('|")(?!(http:\/\/|https:\/\/)).*('|")$/i;
|
||||
fileStrings = fileStrings || [];
|
||||
fileStrings.forEach(fileString => {
|
||||
//if string does not begin with http or https
|
||||
if (fileString.match(fileStringRegex)) {
|
||||
const filePath = fileString.substr(1, fileString.length - 2);
|
||||
const resolvedFile = resolvePathToFile(filePath, files);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
newContent = newContent.replace(filePath,resolvedFile.url);
|
||||
} else if (resolvedFile.name.match(/(.+\.json$|.+\.txt$|.+\.csv$)/i)) {
|
||||
let resolvedFilePath = filePath;
|
||||
if (resolvedFilePath.startsWith('.')) {
|
||||
resolvedFilePath = resolvedFilePath.substr(1);
|
||||
}
|
||||
while (resolvedFilePath.startsWith('/')) {
|
||||
resolvedFilePath = resolvedFilePath.substr(1);
|
||||
}
|
||||
newContent = newContent.replace(filePath, `/api/projects/${projectId}/${resolvedFilePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return newContent;
|
||||
}
|
||||
|
||||
export function injectMediaUrls(filesToInject, allFiles, projectId) {
|
||||
filesToInject.forEach((file, index) => {
|
||||
file.content = resolveLinksInString(file.content, allFiles, projectId);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
|
||||
const elements = sketchDoc.querySelectorAll(`[${attr}]`);
|
||||
const elementsArray = Array.prototype.slice.call(elements);
|
||||
elementsArray.forEach(element => {
|
||||
if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) {
|
||||
const resolvedFile = resolvePathToFile(element.getAttribute(attr), files);
|
||||
if (resolvedFile) {
|
||||
element.setAttribute(attr, resolvedFile.url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveScripts(sketchDoc, files, projectId) {
|
||||
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);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
script.setAttribute('src', resolvedFile.url);
|
||||
} else {
|
||||
script.removeAttribute('src');
|
||||
script.innerHTML = resolvedFile.content;
|
||||
}
|
||||
}
|
||||
} else if (!(script.getAttribute('src') && script.getAttribute('src').match(EXTERNAL_LINK_REGEX) !== null)) {
|
||||
script.innerHTML = resolveLinksInString(script.innerHTML, files, projectId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveStyles(sketchDoc, files, projectId) {
|
||||
const inlineCSSInHTML = sketchDoc.getElementsByTagName('style');
|
||||
const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML);
|
||||
inlineCSSInHTMLArray.forEach(style => {
|
||||
style.innerHTML = resolveLinksInString(style.innerHTML, files, projectId);
|
||||
});
|
||||
|
||||
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);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
css.setAttribute('href', resolvedFile.url);
|
||||
} else {
|
||||
const style = sketchDoc.createElement('style');
|
||||
style.innerHTML = `\n${resolvedFile.content}`;
|
||||
sketchDoc.getElementsByTagName("head")[0].appendChild(style);
|
||||
css.parentNode.removeChild(css);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
35
static/hijackConsole.js
Normal file
35
static/hijackConsole.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
var iframeWindow = window;
|
||||
var originalConsole = iframeWindow.console;
|
||||
iframeWindow.console = {};
|
||||
|
||||
var methods = [
|
||||
'debug', 'clear', 'error', 'info', 'log', 'warn'
|
||||
];
|
||||
|
||||
var consoleBuffer = [];
|
||||
var LOGWAIT = 500;
|
||||
|
||||
methods.forEach( function(method) {
|
||||
iframeWindow.console[method] = function() {
|
||||
originalConsole[method].apply(originalConsole, arguments);
|
||||
|
||||
var args = Array.from(arguments);
|
||||
args = args.map(function(i) {
|
||||
// catch objects
|
||||
return (typeof i === 'string') ? i : JSON.stringify(i);
|
||||
});
|
||||
|
||||
consoleBuffer.push({
|
||||
method: method,
|
||||
arguments: args,
|
||||
source: 'sketch'
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
if (consoleBuffer.length > 0) {
|
||||
window.parent.postMessage(consoleBuffer, '*');
|
||||
consoleBuffer.length = 0;
|
||||
}
|
||||
}, LOGWAIT);
|
|
@ -22,7 +22,7 @@ module.exports = {
|
|||
extensions: ['', '.js', '.jsx'],
|
||||
modules: [
|
||||
'client',
|
||||
'node_modules',
|
||||
'node_modules'
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
|
|
Loading…
Reference in a new issue