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:
Cassie Tarakajian 2016-11-16 13:12:36 -05:00
parent ced885d03f
commit 04922522cc
15 changed files with 384 additions and 245 deletions

View file

@ -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>

View file

@ -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>}

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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" />

View file

@ -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>

View file

@ -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,

View 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 */

View file

@ -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",

View file

@ -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);
});
}

View file

@ -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;
}

View 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
View 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);

View file

@ -22,7 +22,7 @@ module.exports = {
extensions: ['', '.js', '.jsx'],
modules: [
'client',
'node_modules',
'node_modules'
]
},
plugins: [