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: [