From 04922522cc311e9ee0ac218423af54bb3d0de4dc Mon Sep 17 00:00:00 2001
From: Cassie Tarakajian
Date: Wed, 16 Nov 2016 13:12:36 -0500
Subject: [PATCH] cool to share some of this code between client and server
Squashed commit of the following:
commit fb5e82cea930b011792983c7d1cc9f6ecacc7dd4
Author: Cassie Tarakajian
Date: Wed Nov 16 12:28:10 2016 -0500
add server side rendering, untested
commit 5c60fb30c46ea49a8d9a0ecb56f39ec778464a8b
Author: Cassie Tarakajian
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
Date: Tue Nov 15 16:30:09 2016 -0500
remove passing jsFiles and cssFiles to PreviewFrame, fix rendering bug
commit 88c56fd36d3a8d88902c79642171988ce37825f2
Author: Cassie Tarakajian
Date: Tue Nov 15 16:21:59 2016 -0500
code cleanup, untested
commit 82e5dcf8bca461892f1daf06d38f1eaebe72983f
Author: Cassie Tarakajian
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
Date: Tue Nov 15 14:43:38 2016 -0500
add almost full code to create preview html correctly, untested
commit 12f61b2a1aed4607fab24d01572b647ca6210262
Author: Cassie Tarakajian
Date: Wed Nov 2 17:09:26 2016 -0400
refactor some of the preview html generation code
commit 111825846703d5c8959cb18795a3aadb7ebe505c
Author: Cassie Tarakajian
Date: Wed Nov 2 11:06:36 2016 -0400
add comments as plan of action
commit 1cc2cf5203674732b4057382f1937de38b687078
Author: Cassie Tarakajian
Date: Thu Oct 27 19:34:55 2016 -0400
add href parsing
commit e67189298cda9b70645f454ecd541a363980f0e4
Author: Cassie Tarakajian
Date: Thu Oct 27 10:48:36 2016 -0400
continue parsing html
commit 1458fb940a15a3dc5d74890211a3073e920b84b8
Author: Cassie Tarakajian
Date: Wed Oct 26 17:40:31 2016 -0400
start to add html parsing
---
client/modules/IDE/components/LoginForm.jsx | 5 +-
client/modules/IDE/components/NewFileForm.jsx | 3 +-
.../modules/IDE/components/NewFolderForm.jsx | 3 +-
.../IDE/components/NewPasswordForm.jsx | 5 +-
.../modules/IDE/components/PreviewFrame.jsx | 316 ++++++++++--------
.../IDE/components/ResetPasswordForm.jsx | 3 +-
client/modules/IDE/components/SignupForm.jsx | 9 +-
client/modules/IDE/pages/IDEView.jsx | 8 +-
client/utils/reduxFormUtils.js | 16 +
package.json | 9 +-
server/controllers/embed.controller.js | 61 +---
server/utils/filePath.js | 52 ++-
server/utils/previewGeneration.js | 102 ++++++
static/hijackConsole.js | 35 ++
webpack.config.dev.js | 2 +-
15 files changed, 384 insertions(+), 245 deletions(-)
create mode 100644 client/utils/reduxFormUtils.js
create mode 100644 server/utils/previewGeneration.js
create mode 100644 static/hijackConsole.js
diff --git a/client/modules/IDE/components/LoginForm.jsx b/client/modules/IDE/components/LoginForm.jsx
index b6e2602d..707396b1 100644
--- a/client/modules/IDE/components/LoginForm.jsx
+++ b/client/modules/IDE/components/LoginForm.jsx
@@ -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 && {email.error}}
@@ -20,7 +21,7 @@ function LoginForm(props) {
aria-label="password"
type="password"
placeholder="Password"
- {...password}
+ {...domOnlyProps(password)}
/>
{password.touched && password.error && {password.error}}
diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx
index 83a623fa..3e0c6059 100644
--- a/client/modules/IDE/components/NewFileForm.jsx
+++ b/client/modules/IDE/components/NewFileForm.jsx
@@ -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)}
/>
{name.touched && name.error && {name.error}}
diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx
index 0b6e3696..0678d971 100644
--- a/client/modules/IDE/components/NewFolderForm.jsx
+++ b/client/modules/IDE/components/NewFolderForm.jsx
@@ -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)}
/>
diff --git a/client/modules/IDE/components/NewPasswordForm.jsx b/client/modules/IDE/components/NewPasswordForm.jsx
index 1a7bdd90..27c13033 100644
--- a/client/modules/IDE/components/NewPasswordForm.jsx
+++ b/client/modules/IDE/components/NewPasswordForm.jsx
@@ -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 && {password.error}}
@@ -20,7 +21,7 @@ function NewPasswordForm(props) {
type="password"
placeholder="Confirm Password"
aria-label="confirm password"
- {...confirmPassword}
+ {...domOnlyProps(confirmPassword)}
/>
{confirmPassword.touched && confirmPassword.error && {confirmPassword.error}}
diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx
index 1f39d492..56113871 100644
--- a/client/modules/IDE/components/PreviewFrame.jsx
+++ b/client/modules/IDE/components/PreviewFrame.jsx
@@ -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 = ``;
- return s;
-}
-
function hijackConsoleErrorsScript(offs) {
- const s = ``;
+ `;
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(`([\s\S]*?)<\/script>`, 'gmi');
- let replacementString;
- if (jsFile.url) {
- replacementString = ``;
- } else {
- replacementString = ``;
+ 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(``, '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 === 1 || this.props.isTextOutputPlaying) {
- htmlHeadContents += '\n';
- htmlHeadContents += '\n';
- htmlHeadContents += '\n';
- htmlHeadContents += '\n';
- htmlHeadContents += '';
- } else if (this.props.textOutput === 2 || this.props.isTextOutputPlaying) {
- htmlHeadContents += '\n';
- htmlHeadContents += '\n';
- htmlHeadContents += '\n';
- htmlHeadContents += '\n';
- htmlHeadContents += '';
- } else if (this.props.textOutput === 3 || this.props.isTextOutputPlaying) {
- htmlHeadContents += '\n';
- htmlHeadContents += '\n';
+ scriptsToInject = scriptsToInject.concat(interceptorScripts);
}
- htmlFile = htmlFile.replace(/(?:)([\s\S]*?)(?:<\/head>)/gmi, `\n${htmlHeadContents}\n`);
+ 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 = `\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 `\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,
diff --git a/client/modules/IDE/components/ResetPasswordForm.jsx b/client/modules/IDE/components/ResetPasswordForm.jsx
index 48ba0666..0c635c94 100644
--- a/client/modules/IDE/components/ResetPasswordForm.jsx
+++ b/client/modules/IDE/components/ResetPasswordForm.jsx
@@ -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)}
/>
diff --git a/client/modules/IDE/components/SignupForm.jsx b/client/modules/IDE/components/SignupForm.jsx
index 10dfac8b..c4bfbaf3 100644
--- a/client/modules/IDE/components/SignupForm.jsx
+++ b/client/modules/IDE/components/SignupForm.jsx
@@ -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 && {username.error}}
@@ -20,7 +21,7 @@ function SignupForm(props) {
aria-label="email"
type="text"
placeholder="Email"
- {...email}
+ {...domOnlyProps(email)}
/>
{email.touched && email.error && {email.error}}
@@ -30,7 +31,7 @@ function SignupForm(props) {
aria-label="password"
type="password"
placeholder="Password"
- {...password}
+ {...domOnlyProps(password)}
/>
{password.touched && password.error && {password.error}}
@@ -40,7 +41,7 @@ function SignupForm(props) {
type="password"
placeholder="Confirm Password"
aria-label="confirm password"
- {...confirmPassword}
+ {...domOnlyProps(confirmPassword)}
/>
{confirmPassword.touched && confirmPassword.error && {confirmPassword.error}}
diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx
index 0f2a654c..ee4e78ef 100644
--- a/client/modules/IDE/pages/IDEView.jsx
+++ b/client/modules/IDE/pages/IDEView.jsx
@@ -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 {
file.isSelectedFile),
htmlFile: getHTMLFile(state.files),
- jsFiles: getJSFiles(state.files),
- cssFiles: getCSSFiles(state.files),
ide: state.ide,
preferences: state.preferences,
editorAccessibility: state.editorAccessibility,
diff --git a/client/utils/reduxFormUtils.js b/client/utils/reduxFormUtils.js
new file mode 100644
index 00000000..8b4b1205
--- /dev/null
+++ b/client/utils/reduxFormUtils.js
@@ -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 */
diff --git a/package.json b/package.json
index 9f14fed7..109826bb 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/server/controllers/embed.controller.js b/server/controllers/embed.controller.js
index c1dc431a..803b20ae 100644
--- a/server/controllers/embed.controller.js
+++ b/server/controllers/embed.controller.js
@@ -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(`([\s\S]*?)<\/script>`, 'gmi');
- const replacementString = ``;
- htmlFile = htmlFile.replace(fileRegex, replacementString);
+ res.send(serializeDocument(sketchDoc));
});
-
- cssFiles.forEach(cssFile => {
- const fileName = escapeStringRegexp(cssFile.name);
- const fileRegex = new RegExp(``, 'gmi');
- htmlFile = htmlFile.replace(fileRegex, ``);
- });
-
- res.send(htmlFile);
});
}
\ No newline at end of file
diff --git a/server/utils/filePath.js b/server/utils/filePath.js
index 4434daf1..519d1c37 100644
--- a/server/utils/filePath.js
+++ b/server/utils/filePath.js
@@ -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;
-}
\ No newline at end of file
+}
diff --git a/server/utils/previewGeneration.js b/server/utils/previewGeneration.js
new file mode 100644
index 00000000..9e1abf91
--- /dev/null
+++ b/server/utils/previewGeneration.js
@@ -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);
+ }
+ }
+ }
+ });
+ }
diff --git a/static/hijackConsole.js b/static/hijackConsole.js
new file mode 100644
index 00000000..9ae14790
--- /dev/null
+++ b/static/hijackConsole.js
@@ -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);
\ No newline at end of file
diff --git a/webpack.config.dev.js b/webpack.config.dev.js
index b19e09bf..e8d32bc2 100644
--- a/webpack.config.dev.js
+++ b/webpack.config.dev.js
@@ -22,7 +22,7 @@ module.exports = {
extensions: ['', '.js', '.jsx'],
modules: [
'client',
- 'node_modules',
+ 'node_modules'
]
},
plugins: [