Initial preview rendering
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
|
"es6": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"mocha": true,
|
"mocha": true,
|
||||||
"node": true
|
"node": true
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,6 +44,7 @@ public/js/main*
|
|||||||
public/js/commonFramework*
|
public/js/commonFramework*
|
||||||
public/js/sandbox*
|
public/js/sandbox*
|
||||||
public/js/iFrameScripts*
|
public/js/iFrameScripts*
|
||||||
|
public/js/frame-runner*
|
||||||
public/js/plugin*
|
public/js/plugin*
|
||||||
public/js/vendor*
|
public/js/vendor*
|
||||||
public/js/faux*
|
public/js/faux*
|
||||||
|
70
client/frame-runner.js
Normal file
70
client/frame-runner.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var common = parent.__common;
|
||||||
|
var Rx = parent.Rx;
|
||||||
|
|
||||||
|
common.getJsOutput = function evalJs(source = '') {
|
||||||
|
if (window.__err || !common.shouldRun()) {
|
||||||
|
return window.__err || 'source disabled';
|
||||||
|
}
|
||||||
|
let output;
|
||||||
|
try {
|
||||||
|
/* eslint-disable no-eval */
|
||||||
|
output = eval(source);
|
||||||
|
/* eslint-enable no-eval */
|
||||||
|
} catch (e) {
|
||||||
|
window.__err = e;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
common.runTests$ = function runTests$({ tests = [], source }) {
|
||||||
|
const editor = { getValue() { return source; } };
|
||||||
|
if (window.__err) {
|
||||||
|
return Rx.Observable.throw(window.__err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through the test one at a time
|
||||||
|
// on new stacks
|
||||||
|
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||||
|
// add delay here for firefox to catch up
|
||||||
|
.delay(100)
|
||||||
|
.map(({ text, testString }) => {
|
||||||
|
const newTest = { text, testString };
|
||||||
|
let test;
|
||||||
|
try {
|
||||||
|
/* eslint-disable no-eval */
|
||||||
|
test = eval(testString);
|
||||||
|
/* eslint-enable no-eval */
|
||||||
|
if (typeof test === 'function') {
|
||||||
|
// maybe sync/promise/observable
|
||||||
|
if (test.length === 0) {
|
||||||
|
test();
|
||||||
|
}
|
||||||
|
// callback test
|
||||||
|
if (test.length === 1) {
|
||||||
|
console.log('callback test');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
newTest.err = e.message.split(':').shift();
|
||||||
|
}
|
||||||
|
return newTest;
|
||||||
|
})
|
||||||
|
// gather tests back into an array
|
||||||
|
.toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
// used when updating preview without running tests
|
||||||
|
common.checkPreview$ = function checkPreview$(args) {
|
||||||
|
if (window.__err) {
|
||||||
|
return Rx.Observable.throw(window.__err);
|
||||||
|
}
|
||||||
|
return Rx.Observable.just(args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// now that the runPreviewTest$ is defined
|
||||||
|
// we set the subject to true
|
||||||
|
// this will let the updatePreview
|
||||||
|
// script now that we are ready.
|
||||||
|
common.testFrameReady$.onNext(true);
|
||||||
|
});
|
@@ -1,23 +1,15 @@
|
|||||||
/* eslint-disable no-undef, no-unused-vars, no-native-reassign */
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// the $ on the iframe window object is the same
|
var common = parent.__common;
|
||||||
// as the one used on the main site, but
|
|
||||||
// uses the iframe document as the context
|
|
||||||
window.$(document).ready(function() {
|
|
||||||
var _ = parent._;
|
|
||||||
var Rx = parent.Rx;
|
var Rx = parent.Rx;
|
||||||
var chai = parent.chai;
|
|
||||||
var assert = chai.assert;
|
|
||||||
var tests = parent.tests;
|
|
||||||
var common = parent.common;
|
|
||||||
|
|
||||||
common.getJsOutput = function evalJs(code = '') {
|
common.getJsOutput = function evalJs(source = '') {
|
||||||
if (window.__err || !common.shouldRun()) {
|
if (window.__err || !common.shouldRun()) {
|
||||||
return window.__err || 'code disabled';
|
return window.__err || 'source disabled';
|
||||||
}
|
}
|
||||||
let output;
|
let output;
|
||||||
try {
|
try {
|
||||||
/* eslint-disable no-eval */
|
/* eslint-disable no-eval */
|
||||||
output = eval(code);
|
output = eval(source);
|
||||||
/* eslint-enable no-eval */
|
/* eslint-enable no-eval */
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.__err = e;
|
window.__err = e;
|
||||||
@@ -25,48 +17,41 @@ window.$(document).ready(function() {
|
|||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
common.runPreviewTests$ =
|
common.runTests$ = function runTests$({ tests = [], source }) {
|
||||||
function runPreviewTests$({
|
const editor = { getValue() { return source; } };
|
||||||
tests = [],
|
|
||||||
originalCode,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const code = originalCode;
|
|
||||||
const editor = { getValue() { return originalCode; } };
|
|
||||||
if (window.__err) {
|
if (window.__err) {
|
||||||
return Rx.Observable.throw(window.__err);
|
return Rx.Observable.throw(window.__err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate throught the test one at a time
|
// Iterate through the test one at a time
|
||||||
// on new stacks
|
// on new stacks
|
||||||
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||||
// add delay here for firefox to catch up
|
// add delay here for firefox to catch up
|
||||||
.delay(100)
|
.delay(100)
|
||||||
.map(test => {
|
.map(({ text, testString }) => {
|
||||||
const userTest = {};
|
const newTest = { text, testString };
|
||||||
|
let test;
|
||||||
try {
|
try {
|
||||||
/* eslint-disable no-eval */
|
/* eslint-disable no-eval */
|
||||||
eval(test);
|
test = eval(testString);
|
||||||
/* eslint-enable no-eval */
|
/* eslint-enable no-eval */
|
||||||
|
if (typeof test === 'function') {
|
||||||
|
// maybe sync/promise/observable
|
||||||
|
if (test.length === 0) {
|
||||||
|
test();
|
||||||
|
}
|
||||||
|
// callback test
|
||||||
|
if (test.length === 1) {
|
||||||
|
console.log('callback test');
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
userTest.err = e.message.split(':').shift();
|
newTest.err = e.message.split(':').shift();
|
||||||
} finally {
|
|
||||||
if (!test.match(/message: /g)) {
|
|
||||||
// assumes test does not contain arrays
|
|
||||||
// This is a patch until all test fall into this pattern
|
|
||||||
userTest.text = test
|
|
||||||
.split(',')
|
|
||||||
.pop();
|
|
||||||
userTest.text = 'message: ' + userTest.text + '\');';
|
|
||||||
} else {
|
|
||||||
userTest.text = test;
|
|
||||||
}
|
}
|
||||||
}
|
return newTest;
|
||||||
return userTest;
|
|
||||||
})
|
})
|
||||||
// gather tests back into an array
|
// gather tests back into an array
|
||||||
.toArray()
|
.toArray();
|
||||||
.map(tests => ({ ...rest, tests, originalCode }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// used when updating preview without running tests
|
// used when updating preview without running tests
|
||||||
@@ -81,5 +66,5 @@ window.$(document).ready(function() {
|
|||||||
// we set the subject to true
|
// we set the subject to true
|
||||||
// this will let the updatePreview
|
// this will let the updatePreview
|
||||||
// script now that we are ready.
|
// script now that we are ready.
|
||||||
common.previewReady$.onNext(true);
|
common.testFrameReady$.onNext(true);
|
||||||
});
|
});
|
||||||
|
@@ -51,7 +51,7 @@ createApp({
|
|||||||
serviceOptions,
|
serviceOptions,
|
||||||
initialState,
|
initialState,
|
||||||
middlewares: [ routingMiddleware ],
|
middlewares: [ routingMiddleware ],
|
||||||
sagas,
|
sagas: [...sagas ],
|
||||||
sagaOptions,
|
sagaOptions,
|
||||||
reducers: { routing },
|
reducers: { routing },
|
||||||
enhancers: [ devTools ]
|
enhancers: [ devTools ]
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
import loopProtect from 'loopProtect';
|
|
||||||
|
|
||||||
loopProtect.hit = function hit(line) {
|
|
||||||
var err = 'Error: Exiting potential infinite loop at line ' +
|
|
||||||
line +
|
|
||||||
'. To disable loop protection, write: \n\\/\\/ noprotect\nas the first' +
|
|
||||||
'line. Beware that if you do have an infinite loop in your code' +
|
|
||||||
'this will crash your browser.';
|
|
||||||
console.error(err);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Observable[Observable[File]]::addLoopProtect() => Observable[String]
|
|
||||||
export default function addLoopProtect() {
|
|
||||||
const source = this;
|
|
||||||
return source.map(files$ => files$.map(file => {
|
|
||||||
if (file.extname === 'js') {
|
|
||||||
file.contents = loopProtect(file.contents);
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
}));
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
import createTypes from '../../common/app/utils/create-types';
|
|
||||||
const filterTypes = [
|
|
||||||
execute
|
|
||||||
];
|
|
||||||
export default function executeChallengeSaga(action$, getState) {
|
|
||||||
return action$
|
|
||||||
.filter(({ type }) => filterTypes.some(_type => _type === type))
|
|
||||||
.map(action => {
|
|
||||||
if (action.type === execute) {
|
|
||||||
const editors = getState().editors;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@@ -1,14 +1,14 @@
|
|||||||
import { helpers, Observable } from 'rx';
|
import { helpers, Observable } from 'rx';
|
||||||
|
|
||||||
const throwForJsHtml = {
|
const throwForJsHtml = {
|
||||||
extname: /js|html/,
|
ext: /js|html/,
|
||||||
throwers: [
|
throwers: [
|
||||||
{
|
{
|
||||||
name: 'multiline-comment',
|
name: 'multiline-comment',
|
||||||
description: 'Detect if a JS multi-line comment is left open',
|
description: 'Detect if a JS multi-line comment is left open',
|
||||||
thrower: function checkForComments({ content }) {
|
thrower: function checkForComments({ contents }) {
|
||||||
const openingComments = content.match(/\/\*/gi);
|
const openingComments = contents.match(/\/\*/gi);
|
||||||
const closingComments = content.match(/\*\//gi);
|
const closingComments = contents.match(/\*\//gi);
|
||||||
if (
|
if (
|
||||||
openingComments &&
|
openingComments &&
|
||||||
(!closingComments || openingComments.length > closingComments.length)
|
(!closingComments || openingComments.length > closingComments.length)
|
||||||
@@ -20,8 +20,8 @@ const throwForJsHtml = {
|
|||||||
name: 'nested-jQuery',
|
name: 'nested-jQuery',
|
||||||
description: 'Nested dollar sign calls breaks browsers',
|
description: 'Nested dollar sign calls breaks browsers',
|
||||||
detectUnsafeJQ: /\$\s*?\(\s*?\$\s*?\)/gi,
|
detectUnsafeJQ: /\$\s*?\(\s*?\$\s*?\)/gi,
|
||||||
thrower: function checkForNestedJquery({ content }) {
|
thrower: function checkForNestedJquery({ contents }) {
|
||||||
if (content.match(this.detectUnsafeJQ)) {
|
if (contents.match(this.detectUnsafeJQ)) {
|
||||||
throw new Error('Unsafe $($)');
|
throw new Error('Unsafe $($)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,10 +29,10 @@ const throwForJsHtml = {
|
|||||||
name: 'unfinished-function',
|
name: 'unfinished-function',
|
||||||
description: 'lonely function keywords breaks browsers',
|
description: 'lonely function keywords breaks browsers',
|
||||||
detectFunctionCall: /function\s*?\(|function\s+\w+\s*?\(/gi,
|
detectFunctionCall: /function\s*?\(|function\s+\w+\s*?\(/gi,
|
||||||
thower: function checkForUnfinishedFunction({ content: code }) {
|
thrower: function checkForUnfinishedFunction({ contents }) {
|
||||||
if (
|
if (
|
||||||
code.match(/function/g) &&
|
contents.match(/function/g) &&
|
||||||
!code.match(this.detectFunctionCall)
|
!contents.match(this.detectFunctionCall)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'SyntaxError: Unsafe or unfinished function declaration'
|
'SyntaxError: Unsafe or unfinished function declaration'
|
||||||
@@ -43,8 +43,8 @@ const throwForJsHtml = {
|
|||||||
name: 'unsafe console call',
|
name: 'unsafe console call',
|
||||||
description: 'console call stops tests scripts from running',
|
description: 'console call stops tests scripts from running',
|
||||||
detectUnsafeConsoleCall: /if\s\(null\)\sconsole\.log\(1\);/gi,
|
detectUnsafeConsoleCall: /if\s\(null\)\sconsole\.log\(1\);/gi,
|
||||||
thrower: function checkForUnsafeConsole({ content }) {
|
thrower: function checkForUnsafeConsole({ contents }) {
|
||||||
if (content.match(this.detectUnsafeConsoleCall)) {
|
if (contents.match(this.detectUnsafeConsoleCall)) {
|
||||||
throw new Error('Invalid if (null) console.log(1); detected');
|
throw new Error('Invalid if (null) console.log(1); detected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,17 +52,17 @@ const throwForJsHtml = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function pretester() {
|
export default function throwers() {
|
||||||
const source = this;
|
const source = this;
|
||||||
return source.map(file$ => file$.flatMap(file => {
|
return source.map(file$ => file$.flatMap(file => {
|
||||||
if (!throwForJsHtml.extname.test(file.extname)) {
|
if (!throwForJsHtml.ext.test(file.ext)) {
|
||||||
return Observable.just(file);
|
return Observable.just(file);
|
||||||
}
|
}
|
||||||
return Observable.from(throwForJsHtml.throwers)
|
return Observable.from(throwForJsHtml.throwers)
|
||||||
.flatMap(({ thrower }) => {
|
.flatMap(context => {
|
||||||
try {
|
try {
|
||||||
let finalObs;
|
let finalObs;
|
||||||
const maybeObservableOrPromise = thrower(file);
|
const maybeObservableOrPromise = context.thrower(file);
|
||||||
if (helpers.isPromise(maybeObservableOrPromise)) {
|
if (helpers.isPromise(maybeObservableOrPromise)) {
|
||||||
finalObs = Observable.fromPromise(maybeObservableOrPromise);
|
finalObs = Observable.fromPromise(maybeObservableOrPromise);
|
||||||
} else if (Observable.isObservable(maybeObservableOrPromise)) {
|
} else if (Observable.isObservable(maybeObservableOrPromise)) {
|
37
client/new-framework/transformers.js
Normal file
37
client/new-framework/transformers.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import loopProtect from 'loop-protect';
|
||||||
|
|
||||||
|
loopProtect.hit = function hit(line) {
|
||||||
|
var err = 'Error: Exiting potential infinite loop at line ' +
|
||||||
|
line +
|
||||||
|
'. To disable loop protection, write: \n\\/\\/ noprotect\nas the first' +
|
||||||
|
'line. Beware that if you do have an infinite loop in your code' +
|
||||||
|
'this will crash your browser.';
|
||||||
|
console.error(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformersForHtmlJS = {
|
||||||
|
ext: /html|js/,
|
||||||
|
transformers: [
|
||||||
|
{
|
||||||
|
name: 'add-loop-protect',
|
||||||
|
transformer: function addLoopProtect(file) {
|
||||||
|
file.contents = loopProtect(file.contents);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Observable[Observable[File]]::addLoopProtect() => Observable[String]
|
||||||
|
export default function transformers() {
|
||||||
|
const source = this;
|
||||||
|
return source.map(files$ => files$.flatMap(file => {
|
||||||
|
if (!transformersForHtmlJS.ext.test(file.ext)) {
|
||||||
|
return Observable.just(file);
|
||||||
|
}
|
||||||
|
return Observable.from(transformersForHtmlJS.transformers)
|
||||||
|
.reduce((file, context) => context.transformer(file), file);
|
||||||
|
}));
|
||||||
|
}
|
98
client/sagas/execute-challenge-saga.js
Normal file
98
client/sagas/execute-challenge-saga.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
|
import { ajax$ } from '../../common/utils/ajax-stream';
|
||||||
|
import throwers from '../new-framework/throwers';
|
||||||
|
import transformers from '../new-framework/transformers';
|
||||||
|
import types from '../../common/app/routes/challenges/redux/types';
|
||||||
|
import {
|
||||||
|
frameMain
|
||||||
|
} from '../../common/app/routes/challenges/redux/actions';
|
||||||
|
import { setExt, updateContents } from '../../common/utils/polyvinyl';
|
||||||
|
|
||||||
|
// createFileStream(files: Dictionary[Path, PolyVinyl]) =>
|
||||||
|
// Observable[...Observable[...PolyVinyl]]
|
||||||
|
function createFileStream(files = {}) {
|
||||||
|
return Observable.just(
|
||||||
|
Observable.from(Object.keys(files)).map(key => files[key])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jQuery = {
|
||||||
|
src: '/bower_components/jquery/dist/jquery.js',
|
||||||
|
script: true,
|
||||||
|
type: 'global'
|
||||||
|
};
|
||||||
|
|
||||||
|
const scriptCache = new Map();
|
||||||
|
|
||||||
|
function cacheScript({ src } = {}) {
|
||||||
|
if (!src) {
|
||||||
|
return Observable.throw(new Error('No source provided for script'));
|
||||||
|
}
|
||||||
|
if (scriptCache.has(src)) {
|
||||||
|
return scriptCache.get(src);
|
||||||
|
}
|
||||||
|
const script$ = ajax$(src)
|
||||||
|
.doOnNext(res => {
|
||||||
|
console.log('status', res.status);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Request errror: ' + res.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(({ response }) => response)
|
||||||
|
.map(script => `<script>${script}</script>`)
|
||||||
|
.catch(e => (console.error(e), Observable.just('')))
|
||||||
|
.shareReplay();
|
||||||
|
|
||||||
|
scriptCache.set(src, script$);
|
||||||
|
return script$;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameRunner$ = cacheScript({ src: '/js/frame-runner.js' });
|
||||||
|
|
||||||
|
const htmlCatch = '\n<!-- -->';
|
||||||
|
const jsCatch = '\n;/* */';
|
||||||
|
|
||||||
|
export default function executeChallengeSaga(action$, getState) {
|
||||||
|
return action$
|
||||||
|
.filter(({ type }) => type === types.executeChallenge)
|
||||||
|
.debounce(750)
|
||||||
|
.flatMapLatest(() => {
|
||||||
|
const { files, required = [ jQuery ] } = getState().challengesApp;
|
||||||
|
return createFileStream(files)
|
||||||
|
::throwers()
|
||||||
|
::transformers()
|
||||||
|
// createbuild
|
||||||
|
.flatMap(file$ => file$.reduce((build, file) => {
|
||||||
|
let finalFile;
|
||||||
|
if (file.ext === 'js') {
|
||||||
|
finalFile = setExt('html', updateContents(
|
||||||
|
`<script>${file.contents}${jsCatch}</script>`,
|
||||||
|
file
|
||||||
|
));
|
||||||
|
} else if (file.ext === 'css') {
|
||||||
|
finalFile = setExt('html', updateContents(
|
||||||
|
`<style>${file.contents}</style>`,
|
||||||
|
file
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
finalFile = file;
|
||||||
|
}
|
||||||
|
return build + finalFile.contents + htmlCatch;
|
||||||
|
}, ''))
|
||||||
|
// add required scripts and links here
|
||||||
|
.flatMap(build => {
|
||||||
|
const header$ = Observable.from(required)
|
||||||
|
.flatMap(required => {
|
||||||
|
if (required.script) {
|
||||||
|
return cacheScript(required);
|
||||||
|
}
|
||||||
|
return Observable.just('');
|
||||||
|
})
|
||||||
|
.reduce((header, required) => header + required, '');
|
||||||
|
return Observable.combineLatest(header$, frameRunner$)
|
||||||
|
.map(([ header, frameRunner ]) => header + build + frameRunner);
|
||||||
|
})
|
||||||
|
.map(build => frameMain(build));
|
||||||
|
});
|
||||||
|
}
|
51
client/sagas/frame-saga.js
Normal file
51
client/sagas/frame-saga.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { BehaviorSubject } from 'rx';
|
||||||
|
import types from '../../common/app/routes/challenges/redux/types';
|
||||||
|
|
||||||
|
// we use three different frames to make them all essentially pure functions
|
||||||
|
const mainId = 'fcc-main-frame';
|
||||||
|
/*
|
||||||
|
const outputId = 'fcc-output-frame';
|
||||||
|
const testId = 'fcc-test-frame';
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createFrame(document, id = mainId) {
|
||||||
|
const frame = document.createElement('iframe');
|
||||||
|
frame.id = id;
|
||||||
|
frame.setAttribute('style', 'display: none');
|
||||||
|
document.body.appendChild(frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFrame(frame) {
|
||||||
|
frame.src = 'about:blank';
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFrameDocument(document, id = mainId) {
|
||||||
|
let frame = document.getElementById(id);
|
||||||
|
if (!frame) {
|
||||||
|
frame = createFrame(document, id);
|
||||||
|
} else {
|
||||||
|
refreshFrame(frame);
|
||||||
|
}
|
||||||
|
return frame.contentDocument || frame.contentWindow.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
function frameMain(build, document) {
|
||||||
|
const main = getFrameDocument(document);
|
||||||
|
main.open();
|
||||||
|
main.write(build);
|
||||||
|
main.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function frameSaga(actions$, getState, { window, document }) {
|
||||||
|
window.__common = {};
|
||||||
|
window.__common.outputFrameReady$ = new BehaviorSubject(false);
|
||||||
|
window.__common.testFrameReady$ = new BehaviorSubject(false);
|
||||||
|
return actions$
|
||||||
|
.filter(({ type }) => type === types.frameMain)
|
||||||
|
.map(action => {
|
||||||
|
frameMain(action.payload, document);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
@@ -3,11 +3,15 @@ import titleSaga from './title-saga';
|
|||||||
import localStorageSaga from './local-storage-saga';
|
import localStorageSaga from './local-storage-saga';
|
||||||
import hardGoToSaga from './hard-go-to-saga';
|
import hardGoToSaga from './hard-go-to-saga';
|
||||||
import windowSaga from './window-saga';
|
import windowSaga from './window-saga';
|
||||||
|
import executeChallengeSaga from './execute-challenge-saga';
|
||||||
|
import frameSaga from './frame-saga';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
errSaga,
|
errSaga,
|
||||||
titleSaga,
|
titleSaga,
|
||||||
localStorageSaga,
|
localStorageSaga,
|
||||||
hardGoToSaga,
|
hardGoToSaga,
|
||||||
windowSaga
|
windowSaga,
|
||||||
|
executeChallengeSaga,
|
||||||
|
frameSaga
|
||||||
];
|
];
|
||||||
|
@@ -8,23 +8,23 @@ import Editor from './Editor.jsx';
|
|||||||
import SidePanel from './Side-Panel.jsx';
|
import SidePanel from './Side-Panel.jsx';
|
||||||
import Preview from './Preview.jsx';
|
import Preview from './Preview.jsx';
|
||||||
import { challengeSelector } from '../../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
import { updateFile } from '../../redux/actions';
|
import { executeChallenge, updateFile } from '../../redux/actions';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
state => state.challengesApp.tests,
|
state => state.challengesApp.tests,
|
||||||
state => state.challengesApp.files,
|
state => state.challengesApp.files,
|
||||||
state => state.challengesApp.path,
|
state => state.challengesApp.key,
|
||||||
({ challenge, showPreview, mode }, tests, files = {}, path = '') => ({
|
({ challenge, showPreview, mode }, tests, files = {}, key = '') => ({
|
||||||
content: files[path] && files[path].contents || '',
|
content: files[key] && files[key].contents || '',
|
||||||
file: files[path],
|
file: files[key],
|
||||||
showPreview,
|
showPreview,
|
||||||
mode,
|
mode,
|
||||||
tests
|
tests
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const bindableActions = { updateFile };
|
const bindableActions = { executeChallenge, updateFile };
|
||||||
|
|
||||||
export class Challenge extends PureComponent {
|
export class Challenge extends PureComponent {
|
||||||
static displayName = 'Challenge';
|
static displayName = 'Challenge';
|
||||||
@@ -32,7 +32,9 @@ export class Challenge extends PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
showPreview: PropTypes.bool,
|
showPreview: PropTypes.bool,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
mode: PropTypes.string
|
mode: PropTypes.string,
|
||||||
|
updateFile: PropTypes.func,
|
||||||
|
executeChallenge: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
renderPreview(showPreview) {
|
renderPreview(showPreview) {
|
||||||
@@ -54,7 +56,8 @@ export class Challenge extends PureComponent {
|
|||||||
updateFile,
|
updateFile,
|
||||||
file,
|
file,
|
||||||
mode,
|
mode,
|
||||||
showPreview
|
showPreview,
|
||||||
|
executeChallenge
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -68,6 +71,7 @@ export class Challenge extends PureComponent {
|
|||||||
md={ showPreview ? 5 : 8 }>
|
md={ showPreview ? 5 : 8 }>
|
||||||
<Editor
|
<Editor
|
||||||
content={ content }
|
content={ content }
|
||||||
|
executeChallenge={ executeChallenge }
|
||||||
mode={ mode }
|
mode={ mode }
|
||||||
updateFile={ content => updateFile(content, file) }
|
updateFile={ content => updateFile(content, file) }
|
||||||
/>
|
/>
|
||||||
|
@@ -36,6 +36,7 @@ export class Editor extends PureComponent {
|
|||||||
}
|
}
|
||||||
static displayName = 'Editor';
|
static displayName = 'Editor';
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
executeChallenge: PropTypes.func,
|
||||||
height: PropTypes.number,
|
height: PropTypes.number,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
mode: PropTypes.string,
|
mode: PropTypes.string,
|
||||||
@@ -47,6 +48,40 @@ export class Editor extends PureComponent {
|
|||||||
mode: 'javascript'
|
mode: 'javascript'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
createOptions = createSelector(
|
||||||
|
state => state.options,
|
||||||
|
state => state.executeChallenge,
|
||||||
|
state => state.mode,
|
||||||
|
(options, executeChallenge, mode) => ({
|
||||||
|
...options,
|
||||||
|
mode,
|
||||||
|
extraKeys: {
|
||||||
|
Tab(cm) {
|
||||||
|
if (cm.somethingSelected()) {
|
||||||
|
return cm.indentSelection('add');
|
||||||
|
}
|
||||||
|
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
|
||||||
|
return cm.replaceSelection(spaces);
|
||||||
|
},
|
||||||
|
'Shift-Tab': function(cm) {
|
||||||
|
if (cm.somethingSelected()) {
|
||||||
|
return cm.indentSelection('subtract');
|
||||||
|
}
|
||||||
|
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
|
||||||
|
return cm.replaceSelection(spaces);
|
||||||
|
},
|
||||||
|
'Ctrl-Enter': function() {
|
||||||
|
executeChallenge();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
'Cmd-Enter': function() {
|
||||||
|
executeChallenge();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { updateFile = (() => {}) } = this.props;
|
const { updateFile = (() => {}) } = this.props;
|
||||||
this._subscription = this._editorContent$
|
this._subscription = this._editorContent$
|
||||||
@@ -72,7 +107,7 @@ export class Editor extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { content, height, mode } = this.props;
|
const { executeChallenge, content, height, mode } = this.props;
|
||||||
const style = {};
|
const style = {};
|
||||||
if (height) {
|
if (height) {
|
||||||
style.height = height + 'px';
|
style.height = height + 'px';
|
||||||
@@ -84,7 +119,7 @@ export class Editor extends PureComponent {
|
|||||||
<NoSSR>
|
<NoSSR>
|
||||||
<Codemirror
|
<Codemirror
|
||||||
onChange={ this.handleChange }
|
onChange={ this.handleChange }
|
||||||
options={{ ...options, mode }}
|
options={ this.createOptions({ executeChallenge, mode, options }) }
|
||||||
value={ content } />
|
value={ content } />
|
||||||
</NoSSR>
|
</NoSSR>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
|
|
||||||
|
const mainId = 'fcc-main-frame';
|
||||||
export default class extends PureComponent {
|
export default class extends PureComponent {
|
||||||
static displayName = 'Preview';
|
static displayName = 'Preview';
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ export default class extends PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
<iframe
|
<iframe
|
||||||
className='iphone iframe-scroll'
|
className='iphone iframe-scroll'
|
||||||
id='preview' />
|
id={ mainId } />
|
||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,17 +1,28 @@
|
|||||||
import React from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import { Button, ButtonGroup } from 'react-bootstrap';
|
import { Button, ButtonGroup } from 'react-bootstrap';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
|
|
||||||
export default class extends PureComponent {
|
import { executeChallenge } from '../../redux/actions';
|
||||||
|
|
||||||
|
const bindableActions = { executeChallenge };
|
||||||
|
|
||||||
|
export class ToolPanel extends PureComponent {
|
||||||
static displayName = 'ToolPanel';
|
static displayName = 'ToolPanel';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
executeChallenge: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { executeChallenge } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
block={ true }
|
block={ true }
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='btn-big'>
|
className='btn-big'
|
||||||
|
onClick={ executeChallenge }>
|
||||||
Run tests (ctrl + enter)
|
Run tests (ctrl + enter)
|
||||||
</Button>
|
</Button>
|
||||||
<div className='button-spacer' />
|
<div className='button-spacer' />
|
||||||
@@ -42,3 +53,5 @@ export default class extends PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(null, bindableActions)(ToolPanel);
|
||||||
|
@@ -39,4 +39,11 @@ export const updateFile = createAction(
|
|||||||
types.updateFile,
|
types.updateFile,
|
||||||
(content, file) => updateContents(content, file)
|
(content, file) => updateContents(content, file)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateFiles = createAction(types.updateFiles);
|
export const updateFiles = createAction(types.updateFiles);
|
||||||
|
|
||||||
|
// rechallenge
|
||||||
|
export const executeChallenge = createAction(types.executeChallenge);
|
||||||
|
export const frameMain = createAction(types.frameMain);
|
||||||
|
export const frameOutput = createAction(types.frameOutput);
|
||||||
|
export const frameTests = createAction(types.frameTests);
|
||||||
|
@@ -3,7 +3,13 @@ import { createPoly } from '../../../../utils/polyvinyl';
|
|||||||
|
|
||||||
import types from './types';
|
import types from './types';
|
||||||
import { BONFIRE, HTML, JS } from '../../../utils/challengeTypes';
|
import { BONFIRE, HTML, JS } from '../../../utils/challengeTypes';
|
||||||
import { arrayToString, buildSeed, createTests, getPath } from '../utils';
|
import {
|
||||||
|
arrayToString,
|
||||||
|
buildSeed,
|
||||||
|
createTests,
|
||||||
|
getPreFile,
|
||||||
|
getFileKey
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
challenge: '',
|
challenge: '',
|
||||||
@@ -24,7 +30,7 @@ const mainReducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
refresh: true,
|
refresh: true,
|
||||||
challenge: challenge.dashedName,
|
challenge: challenge.dashedName,
|
||||||
path: getPath(challenge),
|
key: getFileKey(challenge),
|
||||||
tests: createTests(challenge)
|
tests: createTests(challenge)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -57,12 +63,12 @@ const filesReducer = handleActions(
|
|||||||
{
|
{
|
||||||
[types.updateFile]: (state, { payload: file }) => ({
|
[types.updateFile]: (state, { payload: file }) => ({
|
||||||
...state,
|
...state,
|
||||||
[file.path]: file
|
[file.key]: file
|
||||||
}),
|
}),
|
||||||
[types.updateFiles]: (state, { payload: files }) => {
|
[types.updateFiles]: (state, { payload: files }) => {
|
||||||
return files
|
return files
|
||||||
.reduce((files, file) => {
|
.reduce((files, file) => {
|
||||||
files[file.path] = file;
|
files[file.key] = file;
|
||||||
return files;
|
return files;
|
||||||
}, { ...state });
|
}, { ...state });
|
||||||
},
|
},
|
||||||
@@ -77,11 +83,11 @@ const filesReducer = handleActions(
|
|||||||
) {
|
) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const path = getPath(challenge);
|
const preFile = getPreFile(challenge);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[path]: createPoly({
|
[preFile.key]: createPoly({
|
||||||
path,
|
...preFile,
|
||||||
contents: buildSeed(challenge),
|
contents: buildSeed(challenge),
|
||||||
head: arrayToString(challenge.head),
|
head: arrayToString(challenge.head),
|
||||||
tail: arrayToString(challenge.tail)
|
tail: arrayToString(challenge.tail)
|
||||||
|
@@ -17,5 +17,11 @@ export default createTypes([
|
|||||||
|
|
||||||
// files
|
// files
|
||||||
'updateFile',
|
'updateFile',
|
||||||
'updateFiles'
|
'updateFiles',
|
||||||
|
|
||||||
|
// rechallenge
|
||||||
|
'executeChallenge',
|
||||||
|
'frameMain',
|
||||||
|
'frameOutput',
|
||||||
|
'frameTests'
|
||||||
], 'challenges');
|
], 'challenges');
|
||||||
|
@@ -40,13 +40,21 @@ export function buildSeed({ challengeSeed = [] } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pathsMap = {
|
const pathsMap = {
|
||||||
[HTML]: 'main.html',
|
[HTML]: 'html',
|
||||||
[JS]: 'main.js',
|
[JS]: 'js',
|
||||||
[BONFIRE]: 'main.js'
|
[BONFIRE]: 'js'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getPath({ challengeType }) {
|
export function getPreFile({ challengeType }) {
|
||||||
return pathsMap[challengeType] || 'main';
|
return {
|
||||||
|
name: 'index',
|
||||||
|
ext: pathsMap[challengeType] || 'html',
|
||||||
|
key: getFileKey({ challengeType })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileKey({ challengeType }) {
|
||||||
|
return 'index' + (pathsMap[challengeType] || 'html');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTests({ tests = [] }) {
|
export function createTests({ tests = [] }) {
|
||||||
|
@@ -1,25 +1,41 @@
|
|||||||
// originally base off of https://github.com/gulpjs/vinyl
|
// originally base off of https://github.com/gulpjs/vinyl
|
||||||
import path from 'path';
|
|
||||||
import replaceExt from 'replace-ext';
|
|
||||||
import invariant from 'invariant';
|
import invariant from 'invariant';
|
||||||
|
|
||||||
// interface PolyVinyl {
|
// interface PolyVinyl {
|
||||||
// contents: String,
|
// contents: String,
|
||||||
|
// name: String,
|
||||||
|
// ext: String,
|
||||||
// path: String,
|
// path: String,
|
||||||
|
// key: String,
|
||||||
|
// head: String,
|
||||||
|
// tail: String,
|
||||||
// history: [...String],
|
// history: [...String],
|
||||||
// error: Null|Object
|
// error: Null|Object
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// createPoly({
|
// createPoly({
|
||||||
// path: String,
|
// name: String,
|
||||||
|
// ext: String,
|
||||||
// contents: String,
|
// contents: String,
|
||||||
// history?: [...String],
|
// history?: [...String],
|
||||||
// }) => PolyVinyl, throws
|
// }) => PolyVinyl, throws
|
||||||
export function createPoly({ path, contents, history, ...rest } = {}) {
|
export function createPoly({
|
||||||
|
name,
|
||||||
|
ext,
|
||||||
|
contents,
|
||||||
|
history,
|
||||||
|
...rest
|
||||||
|
} = {}) {
|
||||||
invariant(
|
invariant(
|
||||||
typeof path === 'string',
|
typeof name === 'string',
|
||||||
'path must be a string but got %s',
|
'name must be a string but got %s',
|
||||||
path
|
name
|
||||||
|
);
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
typeof ext === 'string',
|
||||||
|
'ext must be a string, but was %s',
|
||||||
|
ext
|
||||||
);
|
);
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
@@ -30,9 +46,12 @@ export function createPoly({ path, contents, history, ...rest } = {}) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
history: Array.isArray(history) ? history : [ path ],
|
history: Array.isArray(history) ? history : [ name + ext ],
|
||||||
path: path,
|
name,
|
||||||
contents: contents,
|
ext,
|
||||||
|
path: name + '.' + ext,
|
||||||
|
key: name + ext,
|
||||||
|
contents,
|
||||||
error: null
|
error: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -41,7 +60,8 @@ export function createPoly({ path, contents, history, ...rest } = {}) {
|
|||||||
export function isPoly(poly) {
|
export function isPoly(poly) {
|
||||||
return poly &&
|
return poly &&
|
||||||
typeof poly.contents === 'string' &&
|
typeof poly.contents === 'string' &&
|
||||||
typeof poly.path === 'string' &&
|
typeof poly.name === 'string' &&
|
||||||
|
typeof poly.ext === 'string' &&
|
||||||
Array.isArray(poly.history);
|
Array.isArray(poly.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,23 +89,25 @@ export function updateContents(contents, poly) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExt(poly) {
|
export function setExt(ext, poly) {
|
||||||
checkPoly(poly);
|
checkPoly(poly);
|
||||||
invariant(
|
|
||||||
!!poly.path,
|
|
||||||
'No path specified! Can not get extname'
|
|
||||||
);
|
|
||||||
return path.extname(poly.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setExt(extname, poly) {
|
|
||||||
invariant(
|
|
||||||
poly.path,
|
|
||||||
'No path specified! Can not set extname',
|
|
||||||
);
|
|
||||||
const newPoly = {
|
const newPoly = {
|
||||||
...poly,
|
...poly,
|
||||||
path: replaceExt(this.path, extname)
|
ext,
|
||||||
|
path: poly.name + '.' + ext,
|
||||||
|
key: poly.name + ext
|
||||||
|
};
|
||||||
|
newPoly.history = [ ...poly.history, newPoly.path ];
|
||||||
|
return newPoly;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setName(name, poly) {
|
||||||
|
checkPoly(poly);
|
||||||
|
const newPoly = {
|
||||||
|
...poly,
|
||||||
|
name,
|
||||||
|
path: name + '.' + poly.ext,
|
||||||
|
key: name + poly.ext
|
||||||
};
|
};
|
||||||
newPoly.history = [ ...poly.history, newPoly.path ];
|
newPoly.history = [ ...poly.history, newPoly.path ];
|
||||||
return newPoly;
|
return newPoly;
|
||||||
|
@@ -113,6 +113,7 @@ var paths = {
|
|||||||
js: [
|
js: [
|
||||||
'client/main.js',
|
'client/main.js',
|
||||||
'client/iFrameScripts.js',
|
'client/iFrameScripts.js',
|
||||||
|
'client/frame-runner.js',
|
||||||
'client/plugin.js'
|
'client/plugin.js'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@@ -98,7 +98,6 @@
|
|||||||
"redux-actions": "^0.9.1",
|
"redux-actions": "^0.9.1",
|
||||||
"redux-epic": "^0.1.1",
|
"redux-epic": "^0.1.1",
|
||||||
"redux-form": "^5.2.3",
|
"redux-form": "^5.2.3",
|
||||||
"replace-ext": "0.0.1",
|
|
||||||
"request": "^2.65.0",
|
"request": "^2.65.0",
|
||||||
"reselect": "^2.0.2",
|
"reselect": "^2.0.2",
|
||||||
"rx": "^4.0.0",
|
"rx": "^4.0.0",
|
||||||
|
@@ -39,7 +39,8 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
codemirror: 'CodeMirror'
|
codemirror: 'CodeMirror',
|
||||||
|
'loop-protect': 'loopProtect'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.optimize.DedupePlugin(),
|
new webpack.optimize.DedupePlugin(),
|
||||||
|
Reference in New Issue
Block a user