├── .jshintrc
├── client
├── src
│ ├── stylesheets
│ │ ├── _shame.sass
│ │ ├── _uncontained.sass
│ │ ├── _typography.sass
│ │ ├── main.sass
│ │ ├── _task_states.sass
│ │ ├── _contained.sass
│ │ ├── _keyframes.sass
│ │ ├── _devices.sass
│ │ ├── _spinner.sass
│ │ ├── _variables.sass
│ │ ├── _transitions.sass
│ │ └── _general.sass
│ └── scripts
│ │ ├── util
│ │ ├── isVowel.js
│ │ ├── fillArray.js
│ │ ├── createPureClass.js
│ │ ├── categorizeChains.js
│ │ ├── chainMatch.js
│ │ ├── makeChainReadable.js
│ │ ├── runCode.js
│ │ └── parseChallenge.js
│ │ ├── actions.js
│ │ ├── main.jsx
│ │ ├── components
│ │ ├── icons
│ │ │ ├── Spinner.jsx
│ │ │ └── X.jsx
│ │ ├── Rule.jsx
│ │ ├── RuleList.jsx
│ │ ├── Editor.jsx
│ │ ├── SuccessScreen.jsx
│ │ ├── UI.jsx
│ │ └── Challenge.jsx
│ │ └── stores
│ │ ├── course.js
│ │ └── challenge.js
└── dist
│ ├── operative.min.js
│ └── challenger.min.css
├── .gitignore
├── bower.json
├── LICENSE
├── README.md
├── paths.js
├── package.json
└── gulpfile.js
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_shame.sass:
--------------------------------------------------------------------------------
1 | // ain't nothin' to be ashamed of
2 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_uncontained.sass:
--------------------------------------------------------------------------------
1 | @import 'devices'
2 | @import 'transitions'
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # system / node
2 | .DS_Store
3 | node_modules
4 |
5 | # TODO
6 | server
7 | test
8 |
9 | # build directories
10 | demo
11 | temp
12 | .publish
13 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_typography.sass:
--------------------------------------------------------------------------------
1 | @import url(http://fonts.googleapis.com/css?family=Roboto:400,700|Inconsolata)
2 |
3 | $fmain: Roboto, Helvetica, 'Helvetica Neue', Arial, sans-serif
4 | $fmono: Inconsolata, monospace
5 |
--------------------------------------------------------------------------------
/client/src/scripts/util/isVowel.js:
--------------------------------------------------------------------------------
1 | // a, e, i, o, u...
2 | // and NEVER y.
3 | //
4 | module.exports = function isVowel (c) {
5 | c = c.toUpperCase();
6 | return c === 'A' || c === 'E' || c === 'I' || c === 'O' || c === 'U';
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/stylesheets/main.sass:
--------------------------------------------------------------------------------
1 | // top line of variables includes _typography.sass;
2 | // top line of typography is css @import statements
3 | @import 'variables'
4 | @import 'keyframes'
5 |
6 | @import 'contained'
7 | @import 'uncontained'
8 | @import 'shame'
9 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_task_states.sass:
--------------------------------------------------------------------------------
1 | .complete
2 | color: $csuccess-dark
3 | background: $csuccess
4 |
5 | .blocked
6 | color: $cdisabled-darkish
7 | background: $cdisabled
8 |
9 | .incomplete
10 | color: $cfailure-dark
11 | background: $cfailure
12 |
--------------------------------------------------------------------------------
/client/src/scripts/actions.js:
--------------------------------------------------------------------------------
1 | // reflux actions
2 | //
3 | var Reflux = require('reflux');
4 |
5 | var actions = Reflux.createActions([
6 | 'loadCourse',
7 | 'challengeCompleted',
8 | 'codeEditUser',
9 | // replaces text in CodeMirror
10 | 'codeEditOverride'
11 | ]);
12 |
13 | module.exports = actions;
14 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_contained.sass:
--------------------------------------------------------------------------------
1 | .challenger
2 | position: fixed
3 | top: 0
4 | bottom: 0
5 | left: 0
6 | right: 0
7 | overflow-x: hidden
8 | color: $ctext
9 | background: rgba(#664dab, 0.69)
10 |
11 | &, & button
12 | font: 16px $fmain
13 |
14 | @import 'spinner'
15 | @import 'general'
16 | @import 'task_states'
17 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_keyframes.sass:
--------------------------------------------------------------------------------
1 | @keyframes rotate
2 | 100%
3 | transform: rotate(360deg)
4 |
5 | @keyframes dash
6 | 0%
7 | stroke-dasharray: 30%, 240%
8 | stroke-dashoffset: 0
9 |
10 | 50%
11 | stroke-dasharray: 240%, 30%
12 | stroke-dashoffset: 0
13 |
14 | 100%
15 | stroke-dasharray: 30%, 240%
16 | stroke-dashoffset: -270%
17 |
--------------------------------------------------------------------------------
/client/src/scripts/util/fillArray.js:
--------------------------------------------------------------------------------
1 | // returns an array of `length`, filled with `value`
2 | //
3 | module.exports = function fillArray (length, value) {
4 | var arr = new Array(length);
5 | while (length--) arr[length] = value;
6 |
7 | return arr;
8 | };
9 |
10 | // RIP:
11 | // Initialclever solution hack, didn't work in IE
12 | // return Array.apply(null, Array(length)).map(() => value);
13 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_devices.sass:
--------------------------------------------------------------------------------
1 | @media screen and (max-width: $wshift)
2 | .challenger
3 | background: #fff
4 |
5 | .challenge
6 | width: 100%
7 | box-shadow: none
8 |
9 | .challenge-frame
10 | padding: 18px 3%
11 |
12 | .challenge-content > *
13 | width: 100%
14 |
15 | .rules
16 | position: static
17 | max-height: none
18 | margin-bottom: 30px
19 |
--------------------------------------------------------------------------------
/client/src/scripts/util/createPureClass.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var PureRenderMixin = require('react/addons').addons.PureRenderMixin;
3 |
4 | function createPureClass (specification) {
5 | if (!specification.mixins){
6 | specification.mixins = [];
7 | }
8 |
9 | specification.mixins.push(PureRenderMixin);
10 | return React.createClass(specification);
11 | }
12 |
13 | // allow for single-line require
14 | module.exports = {React, createPureClass};
15 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_spinner.sass:
--------------------------------------------------------------------------------
1 | .spinner
2 | position: absolute
3 | top: 0
4 | left: 0
5 | display: none
6 | height: 100%
7 | width: 100%
8 |
9 | svg
10 | position: absolute
11 | top: 50%
12 | left: 50%
13 | animation: rotate 2s linear infinite
14 |
15 | circle
16 | stroke: #fff
17 | fill: none
18 | animation: dash 1.5s ease-in-out infinite
19 | stroke-linecap: round
20 |
21 | .spinning .spinner
22 | display: block
23 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_variables.sass:
--------------------------------------------------------------------------------
1 | @import 'typography'
2 |
3 | // colors
4 | $ctext: #212121
5 |
6 | $caccent: #664dab
7 | $caccent-light: $caccent + #828282
8 |
9 | $csuccess: #1de9b6
10 | $csuccess-dark: darken($csuccess, 30%)
11 |
12 | $cfailure: #ff784d
13 | $cfailure-dark: darken($cfailure, 40%)
14 |
15 | $cdisabled: #ccced8
16 | $cdisabled-darkish: darken($cdisabled, 10%)
17 | $cdisabled-dark: darken($cdisabled, 42%)
18 |
19 | // widths
20 | $wshift: 920px
21 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_transitions.sass:
--------------------------------------------------------------------------------
1 | .challenger
2 | .challenge
3 | transition: left 1s
4 |
5 | .challenge-enter
6 | left: 100%
7 |
8 | &.challenge-enter-active
9 | left: 0 // resting state
10 |
11 | .challenge-leave
12 | // remove from flow & vertical center
13 | position: absolute
14 | top: 50%
15 | transform: translate(0, -50%)
16 |
17 | left: 0 // resting state
18 |
19 | &.challenge-leave-active
20 | left: -100%
21 |
--------------------------------------------------------------------------------
/client/src/scripts/util/categorizeChains.js:
--------------------------------------------------------------------------------
1 | // accepts an array of rules and organizes the expression chains
2 | // in an object according to their deepest-nested expression.
3 | //
4 | module.exports = function categorizeChains (rules) {
5 | return rules.reduce((categorized, {chain, index}) => {
6 | // the deepest-nested expression, i.e. the one we're
7 | // looking for
8 | var exp = chain[0];
9 |
10 | if (!categorized[exp]) {
11 | categorized[exp] = [];
12 | }
13 |
14 | categorized[exp].push({
15 | chain: chain.slice(1),
16 | index
17 | });
18 |
19 | return categorized;
20 | }, {});
21 | };
22 |
--------------------------------------------------------------------------------
/client/src/scripts/main.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var UI = require('./components/UI.jsx');
3 | var loadCourse = require('./actions').loadCourse;
4 |
5 | function challenger (course, {
6 | parent = document.body,
7 | onExit = (success) => null,
8 | successText,
9 | }) {
10 | function unmount (success) {
11 | onExit(success);
12 | React.unmountComponentAtNode(container);
13 | parent.removeChild(container);
14 | }
15 |
16 | var container = document.createElement('div');
17 | container.className = 'challenger';
18 | parent.appendChild(container);
19 |
20 | React.render(, container);
21 | loadCourse(course);
22 | }
23 |
24 | module.exports = challenger;
25 |
--------------------------------------------------------------------------------
/client/src/scripts/components/icons/Spinner.jsx:
--------------------------------------------------------------------------------
1 | var {React, createPureClass} = require('../../util/createPureClass.js');
2 |
3 | var Spinner = createPureClass({
4 | propTypes: {
5 | radius: React.PropTypes.number.isRequired
6 | },
7 |
8 | render() {
9 | var r = this.props.radius;
10 | var size = r * 2 + r / 4;
11 | var style = { width: size, height: size, margin: -size / 2 };
12 |
13 | return (
14 |
15 |
23 |
24 | );
25 | }
26 | });
27 |
28 | module.exports = Spinner;
29 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "challenger",
3 | "version": "1.0.2",
4 | "homepage": "https://github.com/rileyjshaw/challenger/",
5 | "authors": [
6 | "rileyjshaw (http://rileyjshaw.com/)"
7 | ],
8 | "description": "Pop-up JavaScript challenges in your browser",
9 | "main": "client/dist/challenger.min.js",
10 | "moduleType": [
11 | "amd",
12 | "globals",
13 | "node"
14 | ],
15 | "keywords": [
16 | "programming",
17 | "challenge",
18 | "education",
19 | "analysis",
20 | "learning",
21 | "sandbox"
22 | ],
23 | "license": "MIT",
24 | "ignore": [
25 | ".DS_Store",
26 | "node_modules",
27 | "bower_components",
28 | "server",
29 | "test",
30 | "demo",
31 | "temp",
32 | ".publish"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/scripts/components/icons/X.jsx:
--------------------------------------------------------------------------------
1 | var {React, createPureClass} = require('../../util/createPureClass.js');
2 |
3 | var style = {
4 | line: {
5 | stroke: '#e8cfff',
6 | strokeWidth: 2,
7 | fill: 'none',
8 | transition: 'stroke 0.2s',
9 | },
10 | };
11 |
12 | var X = createPureClass({
13 | propTypes: {
14 | size: React.PropTypes.number.isRequired
15 | },
16 |
17 | render() {
18 | var s = this.props.size;
19 | var lo = s / 12;
20 | var hi = lo * 11;
21 |
22 | return (
23 |
29 | );
30 | }
31 | });
32 |
33 | module.exports = X;
34 |
--------------------------------------------------------------------------------
/client/src/scripts/util/chainMatch.js:
--------------------------------------------------------------------------------
1 | // checks if a chain matches the current AST node
2 | //
3 | module.exports = function chainMatch (chain, state) {
4 | // fast exit if there's no tree to resolve
5 | if (chain.length === 0) return true;
6 |
7 | state = state
8 | // `state` still contains the matched expression and
9 | // the top-level program node
10 | .slice(1, -1)
11 | // the `type` string is all we care about
12 | .map(node => node.type)
13 | // `state` moves from least -> most nested, which is
14 | // opposite to the direction of `chain`
15 | .reverse();
16 |
17 | var chainLength = chain.length;
18 | var chainIdx = 0;
19 |
20 | for (var i = 0, _len = state.length; i < _len; i++) {
21 | if (chain[chainIdx] === state[i])
22 | if (++chainIdx === chainLength) return true;
23 | }
24 |
25 | return false;
26 | };
27 |
--------------------------------------------------------------------------------
/client/src/scripts/components/Rule.jsx:
--------------------------------------------------------------------------------
1 | var {React, createPureClass} = require('../util/createPureClass.js');
2 | var Spinner = require('./icons/Spinner.jsx');
3 |
4 | var Rule = createPureClass({
5 | propTypes: {
6 | description: React.PropTypes.node.isRequired, // string or array
7 | required: React.PropTypes.bool.isRequired,
8 | present: React.PropTypes.bool.isRequired,
9 | blocked: React.PropTypes.bool,
10 | spins: React.PropTypes.bool,
11 | },
12 |
13 | render() {
14 | var { description, required, present, blocked, spins } = this.props;
15 |
16 | var className = blocked ? 'blocked' + (spins ? ' spinning' : '') :
17 | present === required ? 'complete' : 'incomplete';
18 |
19 | return (
20 |
21 | {description}.
22 |
23 |
24 | );
25 | },
26 | });
27 |
28 | module.exports = Rule;
29 |
--------------------------------------------------------------------------------
/client/src/scripts/util/makeChainReadable.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var isVowel = require('./isVowel');
3 |
4 | // translates an expression chain to plain English
5 | //
6 | module.exports = function (expressionChain, required) {
7 | return [`Program must ${required ? '' : 'not '}`].concat(
8 | expressionChain.map(function (exp, i) {
9 | // add spaces to the expression name and lowercase it
10 | var readableExp = exp.replace(/(.)([A-Z])/g, '$1 $2').toLowerCase();
11 |
12 | return (
13 |
14 | {/* add `contain` for the first expression and `within` subsequently */}
15 | {i ? ' within ' : 'contain '}
16 | {/* prepend with 'a' or 'an', depending on the first character */}
17 | {isVowel(exp[0]) ? 'an ' : 'a '}
18 |
19 | {readableExp}
20 |
21 |
22 | );
23 | })
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/client/src/scripts/stores/course.js:
--------------------------------------------------------------------------------
1 | // holds current course data
2 | //
3 | var Reflux = require('reflux');
4 | var actions = require('../actions');
5 |
6 | var parseChallenge = require('../util/parseChallenge');
7 |
8 | var courseStore = Reflux.createStore({
9 | listenables: actions,
10 | init() { this.rules = {}; },
11 |
12 | updateChallenge(index) {
13 | var newChallenge = this.course[index];
14 |
15 | if (newChallenge) this.trigger(newChallenge);
16 | else this.trigger({ courseCompleted: true });
17 | },
18 |
19 | onLoadCourse(newCourse) {
20 | if (!Array.isArray(newCourse)) newCourse = [newCourse];
21 | this.course = newCourse.map(parseChallenge);
22 | this.challenge = 0;
23 |
24 | this.trigger({ maxIndex: newCourse.length - 1 });
25 | this.updateChallenge(this.challenge);
26 | },
27 |
28 | onChallengeCompleted(code) {
29 | this.course[this.challenge].initialCode = code;
30 | this.updateChallenge(++this.challenge);
31 | },
32 |
33 | });
34 |
35 | module.exports = courseStore;
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 rileyjshaw
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/client/src/scripts/util/runCode.js:
--------------------------------------------------------------------------------
1 | var babel = require('babel');
2 |
3 | // exposes global variable `operative`
4 | require('operative');
5 | operative.setSelfURL('./operative.min.js');
6 |
7 | function runCode (code, verify, trigger) {
8 | var worker = operative(function (__code__, __verify__) {
9 | eval(__code__);
10 | });
11 |
12 | // transform our code string from es6 to es5
13 | var es5 = babel.transform(code, {
14 | ast: false,
15 | blacklist: ['useStrict'],
16 | }).code;
17 |
18 | // ensure that long-running plugins eg. while(1) will
19 | // halt execution after 700ms
20 | var limitExecutionTime = setTimeout(() => {
21 | worker.terminate();
22 | trigger(false);
23 | }, 700);
24 |
25 | // clean up the worker and timeout if we finish early
26 | function earlyExit () {
27 | clearTimeout(limitExecutionTime);
28 | worker.terminate();
29 | }
30 |
31 | var passed = false;
32 | // latchedVerify only needs to be correct once for passed to === true
33 | function latchedVerify (...args) {
34 | if (!passed && verify(...args)) {
35 | earlyExit();
36 | passed = true;
37 | trigger(true);
38 | }
39 | }
40 |
41 | // finally, spawn our worker
42 | worker(es5, latchedVerify);
43 |
44 | return earlyExit;
45 | }
46 |
47 | module.exports = runCode;
48 |
--------------------------------------------------------------------------------
/client/src/scripts/components/RuleList.jsx:
--------------------------------------------------------------------------------
1 | var {React, createPureClass} = require('../util/createPureClass.js');
2 | var Rule = require('./Rule.jsx');
3 |
4 | var makeChainReadable = require('../util/makeChainReadable');
5 |
6 | var RuleList = createPureClass({
7 | propTypes: {
8 | valid: React.PropTypes.bool.isRequired,
9 | checkingOutput: React.PropTypes.bool.isRequired,
10 | rules: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
11 | required: React.PropTypes.arrayOf(React.PropTypes.bool).isRequired,
12 | present: React.PropTypes.arrayOf(React.PropTypes.bool).isRequired
13 | },
14 |
15 | render() {
16 | var {rules, required, present, valid, checkingOutput} = this.props;
17 |
18 | rules = rules.map(function ({type, chain, description}, i) {
19 | var isChain = type === 'expressionChain';
20 | var isOutput = type === 'output';
21 |
22 | return (
23 |
31 | );
32 | });
33 |
34 | rules.unshift(
35 |
41 | );
42 |
43 | return (
44 |
47 | );
48 | },
49 | });
50 |
51 | module.exports = RuleList;
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Challenger v1.0.2
2 | _Pop-up JavaScript challenges in your browser_
3 |
4 | Challenger is a drop-in JavaScript library that adds interactive programming challenges to any page. Challenges are flexible and expressive, and are super simple to write.
5 |
6 | A challenge has requirements based on code structure and program output, and gives users a code editor to experiment in. When new code is written, it's run in a sandbox and the output is analyzed.
7 |
8 | Challenges can be presented as one-off tests or linked together to form courses.
9 |
10 | ## Docs
11 | For full documentation including usage examples, visit the [main project page](http://rileyjshaw.com/challenger).
12 |
13 | ## Roadmap
14 |
15 | - [x] ~~Multiple challenges in a row~~
16 | - [x] ~~Custom rules~~
17 | - [x] ~~Styling~~
18 | - [x] ~~Code evaluation on the client~~
19 | - [ ] Code evaluation on the server
20 | - [x] ~~Add `setup` and `teardown` options to challenge objects~~
21 | - [x] ~~Fix CodeMirror rendering in older versions of Firefox~~
22 | - [ ] **Reduce the bundle size**
23 |
24 | Reducing bundle size important, as we're currently weighing in at ~2M. There's a lot of bloat from redundant dependencies - if anyone has experience with this I'd really appreciate a hand.
25 |
26 | ## Browser support
27 | Tested with [BrowserStack](https://www.browserstack.com/)
28 |
29 | - Chrome 18+
30 | - Firefox 9+
31 | - Opera 15+
32 | - Safari 5.1+
33 | - IE9+
34 | - Mobile Safari
35 |
36 | If you need to support older browsers, include [krisowal's es5-shim](https://github.com/es-shims/es5-shim) along with `es5-sham.js` from the same repository. You might also need to tweak the CSS.
37 |
38 | ## Licence
39 | [MIT](LICENSE)
40 |
41 | ## That's all, folks
42 | [@rileyjshaw](https://twitter.com/rileyjshaw)
43 |
--------------------------------------------------------------------------------
/paths.js:
--------------------------------------------------------------------------------
1 | var paths = {
2 | client: {
3 | dir: './client/src/',
4 | dist: './client/dist/',
5 | scripts: {
6 | dir: './client/src/scripts/',
7 | entry: './client/src/scripts/main.jsx',
8 | all: './client/src/scripts/**/*.{js,jsx}',
9 | },
10 | stylesheets: {
11 | dir: './client/src/stylesheets/',
12 | entry: './client/src/stylesheets/main.sass',
13 | all: './client/src/stylesheets/**/*.sass',
14 | plugins: [
15 | './node_modules/codemirror/lib/codemirror.css',
16 | './node_modules/codemirror/theme/neo.css',
17 | './node_modules/codemirror/addon/lint/lint.css',
18 | ],
19 | },
20 | static: {
21 | all: ['./node_modules/operative/dist/operative.min.js'],
22 | },
23 | temp: './client/temp/',
24 | },
25 | demo: {
26 | dir: './demo/src/',
27 | dist: './demo/dist/',
28 | scripts: {
29 | dir: './demo/src/scripts/',
30 | entry: './demo/src/scripts/main.js',
31 | all: './demo/src/scripts/**/*.js',
32 | },
33 | stylesheets: {
34 | dir: './demo/src/stylesheets/',
35 | entry: './demo/src/stylesheets/main.sass',
36 | all: './demo/src/stylesheets/**/*.sass',
37 | },
38 | static: {
39 | dir: './demo/src/static/',
40 | all: ['./demo/src/static/**/*', './node_modules/operative/dist/operative.min.js'],
41 | },
42 | },
43 | server: {
44 | dir: './server/src/',
45 | dist: './server/dist/',
46 | },
47 | shared: {
48 | dir: './shared/src/',
49 | dist: './shared/dist/',
50 | scripts: {
51 | dir: './shared/src/scripts/',
52 | all: './shared/src/scripts/**/*.js',
53 | },
54 | },
55 | tests: {
56 | dir: './test/',
57 | main: './test/test.js',
58 | },
59 | };
60 |
61 | module.exports = paths;
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "challenger",
3 | "npmName": "challenger",
4 | "npmFileMap": [
5 | {
6 | "basePath": "/client/dist/",
7 | "files": [
8 | "*"
9 | ]
10 | }
11 | ],
12 | "version": "1.0.2",
13 | "description": "Pop-up JavaScript challenges in your browser",
14 | "main": "client/dist/challenger.min.js",
15 | "scripts": {
16 | "test": "mocha"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/rileyjshaw/challenger.git"
21 | },
22 | "keywords": [
23 | "programming",
24 | "challenge",
25 | "education",
26 | "analysis",
27 | "learning",
28 | "sandbox"
29 | ],
30 | "author": "rileyjshaw (http://rileyjshaw.com/)",
31 | "license": "MIT",
32 | "devDependencies": {
33 | "6to5ify": "^4.1.1",
34 | "browserify": "^9.0.8",
35 | "chai": "^2.2.0",
36 | "gulp": "^3.8.11",
37 | "gulp-autoprefixer": "^2.2.0",
38 | "gulp-babel": "^5.1.0",
39 | "gulp-concat": "^2.5.2",
40 | "gulp-gh-pages": "^0.5.1",
41 | "gulp-if": "^1.2.5",
42 | "gulp-jshint": "^1.10.0",
43 | "gulp-load-plugins": "^0.10.0",
44 | "gulp-minify-css": "^1.1.0",
45 | "gulp-mocha": "^2.0.1",
46 | "gulp-rename": "^1.2.2",
47 | "gulp-sass": "^2.1.0",
48 | "gulp-sourcemaps": "^1.5.2",
49 | "gulp-uglify": "^1.2.0",
50 | "gulp-util": "^3.0.4",
51 | "gulp-webserver": "^0.9.0",
52 | "mocha": "^2.2.4",
53 | "reactify": "^1.1.0",
54 | "uglifyify": "^3.0.1",
55 | "vinyl-buffer": "^1.0.0",
56 | "vinyl-source-stream": "^1.1.0"
57 | },
58 | "bugs": {
59 | "url": "https://github.com/rileyjshaw/challenger/issues"
60 | },
61 | "homepage": "http://rileyjshaw.com/challenger",
62 | "dependencies": {
63 | "acorn": "^1.0.3",
64 | "babel": "^5.1.13",
65 | "codemirror": "^5.2.0",
66 | "flatmap": "0.0.3",
67 | "jshint": "^2.7.0",
68 | "operative": "^0.4.4",
69 | "react": "^0.13.2",
70 | "reflux": "^0.2.7"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/client/src/scripts/components/Editor.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var CodeMirror = require('codemirror');
3 |
4 | var Reflux = require('reflux');
5 | var codeEditUser = require('../actions').codeEditUser;
6 | var codeEditOverride = require('../actions').codeEditOverride;
7 | var challengeStore = require('../stores/challenge');
8 |
9 | // set JSHINT as a global...
10 | window.JSHINT = require('jshint').JSHINT;
11 |
12 | // ...so codemirror can access it in the following addons:
13 | require('codemirror/mode/javascript/javascript');
14 | // TODO: turn linting back on (removed for issue #1)
15 | //require('codemirror/addon/lint/lint');
16 | //require('codemirror/addon/lint/javascript-lint');
17 |
18 | var Editor = React.createClass({
19 | mixins: [Reflux.listenTo(challengeStore, 'onChallengeStoreChange')],
20 |
21 | componentDidMount() {
22 | var cm = CodeMirror.fromTextArea(this.getDOMNode(), {
23 | autofocus: true,
24 | lineNumbers: true,
25 | mode: 'javascript',
26 | // lint: { esnext: true },
27 | // gutters: ['CodeMirror-lint-markers'],
28 | styleActiveLine: true,
29 | theme: 'neo',
30 | indentWithTabs: false,
31 | tabSize: 2,
32 | });
33 | this.cm = cm;
34 |
35 | this.getText = cm.doc.getValue.bind(cm.doc);
36 | this.setText = cm.doc.setValue.bind(cm.doc);
37 | cm.on('change', this.onChange);
38 |
39 | codeEditOverride();
40 | this.onChange();
41 | },
42 |
43 | onChallengeStoreChange(newText) {
44 | if (typeof newText === 'string') {
45 | this.setText(newText);
46 |
47 | // set focus to the end of the second last line
48 | this.cm.doc.setCursor(this.cm.doc.lineCount() - 2, 1000);
49 | }
50 | },
51 |
52 | onChange() {
53 | codeEditUser(this.getText());
54 | },
55 |
56 | componentWillUnmount() {
57 | this.cm.off('change', this.onChange);
58 | delete this.getText;
59 | delete this.setText;
60 |
61 | this.cm.toTextArea();
62 | },
63 |
64 | render() {
65 | // textarea will be gobbled up by CodeMirror;
66 | // set it to readOnly to hush the compiler
67 | return (
68 |
69 | );
70 | },
71 | });
72 |
73 | module.exports = Editor;
74 |
--------------------------------------------------------------------------------
/client/src/scripts/components/SuccessScreen.jsx:
--------------------------------------------------------------------------------
1 | var {React, createPureClass} = require('../util/createPureClass.js');
2 |
3 | var SuccessScreen = createPureClass({
4 | propTypes: {
5 | text: React.PropTypes.oneOfType([
6 | React.PropTypes.string,
7 | React.PropTypes.object,
8 | ]),
9 | },
10 |
11 | render() {
12 | var text = this.props.text || {};
13 |
14 | // defaults
15 | var {
16 | top = 'Way to go!',
17 | bottom = 'You just passed the final exercise of this course.'
18 | } = text;
19 |
20 | bottom = typeof text === 'string' ? text : bottom;
21 |
22 | return (
23 |
24 |
{top}
25 |
26 |
{bottom}
27 |
28 | );
29 | },
30 | });
31 |
32 | module.exports = SuccessScreen;
33 |
--------------------------------------------------------------------------------
/client/src/scripts/components/UI.jsx:
--------------------------------------------------------------------------------
1 | var {React, createPureClass} = require('../util/createPureClass.js');
2 | var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
3 |
4 | // reflux
5 | var Reflux = require('reflux');
6 | var courseStore = require('../stores/course');
7 | var challengeStore = require('../stores/challenge');
8 |
9 | // components
10 | var Challenge = require('./Challenge.jsx');
11 |
12 | var UI = createPureClass({
13 | propTypes: {
14 | unmount: React.PropTypes.func.isRequired,
15 | successText: React.PropTypes.oneOfType([
16 | React.PropTypes.string,
17 | React.PropTypes.object,
18 | ]),
19 | },
20 |
21 | mixins: [
22 | Reflux.connect(courseStore),
23 | Reflux.listenTo(challengeStore, 'onChallengeStoreChange')
24 | ],
25 |
26 | getInitialState() {
27 | return {
28 | courseCompleted: false,
29 | index: -1,
30 | maxIndex: -1,
31 | title: '',
32 | description: '',
33 | valid: true,
34 | checkingOutput: false,
35 | rules: [],
36 | required: [],
37 | present: [],
38 | };
39 | },
40 |
41 | // challengeStore can also emit an editorText string after a
42 | // codeEditOverride, so we typecheck before setting state
43 | onChallengeStoreChange(newState) {
44 | if (typeof newState === 'object') {
45 | this.setState(newState);
46 | }
47 | },
48 |
49 | handleKeyDown(e){
50 | // if (e.which === 27) this.unmount();
51 | },
52 |
53 | unmount() { this.props.unmount(this.state.completed) },
54 |
55 | render() {
56 | var {
57 | courseCompleted,
58 | index,
59 | maxIndex,
60 | title,
61 | description,
62 | valid,
63 | checkingOutput,
64 | rules,
65 | required,
66 | present,
67 | } = this.state;
68 |
69 | return (
70 |
75 | {index === -1 ? '' :
76 |
92 | }
93 |
94 | );
95 | },
96 | });
97 |
98 | module.exports = UI;
99 |
--------------------------------------------------------------------------------
/client/src/stylesheets/_general.sass:
--------------------------------------------------------------------------------
1 | .challenge-outer
2 | height: 100%
3 | width: 100%
4 | white-space: nowrap
5 | text-align: center
6 |
7 | &::before
8 | content: ''
9 | display: inline-block
10 | height: 100%
11 | vertical-align: middle
12 |
13 | .challenge
14 | position: relative
15 | display: inline-block
16 | width: 90%
17 | max-width: 1280px
18 | vertical-align: middle
19 | white-space: normal
20 | text-align: left
21 | background: #fff
22 | box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12)
23 |
24 | h1
25 | line-height: 60px
26 | margin: 0
27 | padding: 0 60px 0 30px
28 | color: $caccent-light
29 | background: $caccent
30 |
31 | small
32 | line-height: 0.5em
33 | margin-right: 18px
34 | font-size: 0.5em
35 |
36 | .challenge-frame
37 | padding: 18px 30px
38 |
39 | .challenge-content
40 | position: relative
41 | margin-bottom: 18px
42 |
43 | & > *
44 | width: 48%
45 |
46 | .description
47 | margin-top: 0
48 | font-size: 22px
49 | line-height: 1.46em
50 |
51 | code
52 | padding: 0 0.2em
53 |
54 | .rules
55 | position: absolute
56 | right: 0
57 | bottom: 0
58 | max-height: 100%
59 | overflow-y: auto
60 | margin: 0
61 | padding: 0
62 | list-style: none
63 |
64 | li
65 | position: relative
66 | margin-bottom: 12px
67 | padding: 1.6em 1em
68 | box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12)
69 |
70 | p
71 | margin: 0
72 |
73 | &:last-child
74 | margin-bottom: 0
75 |
76 | code, .CodeMirror
77 | font-family: $fmono
78 |
79 | .CodeMirror
80 | box-sizing: border-box
81 | border: 1px solid $ctext
82 |
83 | .cm-s-neo .CodeMirror-gutters
84 | background: #fff
85 |
86 | .unmount
87 | $size: 36px
88 | position: absolute
89 | top: (60px - $size) / 2
90 | right: (60px - $size) / 2
91 | height: $size
92 | width: $size
93 | padding: 0
94 | border: 0
95 | background: none
96 | cursor: pointer
97 |
98 | &:hover
99 | line
100 | stroke: #fff !important
101 |
102 | .submit
103 | height: 48px
104 | width: 100%
105 | padding: 0.6em 1em
106 | border: 0
107 | border-radius: 0
108 | color: $csuccess-dark
109 | background: $csuccess
110 | cursor: pointer
111 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24)
112 |
113 | &[disabled]
114 | color: $cdisabled-dark
115 | background: $cdisabled
116 | cursor: default
117 |
118 | .successScreen
119 | text-align: center
120 |
121 | .textBefore
122 | margin-bottom: 24px
123 | font-size: 36px
124 |
125 | svg
126 | height: 240px
127 | width: 240px
128 | margin: 24px 0
129 |
130 | .textAfter
131 | margin-bottom: 1.5em
132 | font-size: 24px
133 |
--------------------------------------------------------------------------------
/client/src/scripts/util/parseChallenge.js:
--------------------------------------------------------------------------------
1 | // Accepts a challenge object, containing:
2 | // - whitelist and blacklist arrays
3 | // - a nestedRules object for nested rules
4 | // - a customRules array,
5 | // Returns an object formatted for the UI state:
6 | // {rules[], required[], present[]}.
7 | //
8 | var flatmap = require('flatmap');
9 | var fillArray = require('./fillArray');
10 |
11 | // Recursively walks the nested object to form rule chains.
12 | // rule chains move from most to least nested & start with a bool.
13 | // bool === if chain is required or prohibited in the challenge.
14 | function chainTransform (obj, path = []) {
15 | return flatmap(Object.keys(obj), function (key) {
16 | if (key === 'required') {
17 | // starts array with `true` or `false` and makes the
18 | // deepest-nested expression first in the array
19 | return [path.concat(obj[key]).reverse()];
20 | } else {
21 | return chainTransform(obj[key], path.concat(key));
22 | }
23 | });
24 | }
25 |
26 | function parseChallenge (args, index) {
27 | var {
28 | whitelist = [],
29 | blacklist = [],
30 | nestedRules = {},
31 | customRules = [],
32 | output,
33 | initialCode = '',
34 | } = args;
35 |
36 | var nestedChains = chainTransform(nestedRules);
37 |
38 | var numRules = whitelist.length + blacklist.length +
39 | nestedChains.length + customRules.length;
40 |
41 | var rules = whitelist.map(exp => ({type: 'expressionChain', chain: [exp]}))
42 | // white/blacklist have their expression strings wrapped in [] to match nestedChains
43 | .concat(blacklist.map(exp => ({type: 'expressionChain', chain: [exp]})))
44 | // nested expression chains have arrays of expression strings as values
45 | .concat(nestedChains.map(chain => ({type: 'expressionChain', chain: chain.slice(1)})))
46 | // custom rules have evaluation functions as values
47 | .concat(customRules.map(({description, verify}) => ({type: 'custom', description, verify})));
48 |
49 | var required = whitelist.map(() => true)
50 | .concat(blacklist.map(() => false))
51 | .concat(nestedChains.map(chain => chain[0]))
52 | .concat(customRules.map(() => true));
53 |
54 | if (output) {
55 | output.type = 'output';
56 | rules.push(output);
57 | required.push(true);
58 | ++numRules;
59 | }
60 |
61 | var present = fillArray(numRules, false);
62 |
63 | var title = args.title || (numRules ? 'JavaScript Challenge' : 'Sandbox');
64 | var description = args.description || (numRules ? 'Meet all of the following requirements, then hit Submit to continue.' : 'There aren\'t any requirements in this level; just play around, and hit Submit when you\'re ready to continue.');
65 |
66 | return {
67 | index,
68 | title,
69 | description,
70 | initialCode,
71 | numRules,
72 | rules,
73 | required,
74 | present,
75 | };
76 | }
77 |
78 | module.exports = parseChallenge;
79 |
--------------------------------------------------------------------------------
/client/src/scripts/components/Challenge.jsx:
--------------------------------------------------------------------------------
1 | var {React, createPureClass} = require('../util/createPureClass.js');
2 |
3 | var RuleList = require('./RuleList.jsx');
4 | var SuccessScreen = require('./SuccessScreen.jsx');
5 | var Editor = require('./Editor.jsx');
6 | var X = require('./icons/X.jsx');
7 |
8 | var challengeCompleted = require('../actions').challengeCompleted;
9 |
10 | var Challenge = createPureClass({
11 | propTypes: {
12 | courseCompleted: React.PropTypes.bool.isRequired,
13 | key: React.PropTypes.number.isRequired,
14 | index: React.PropTypes.number.isRequired,
15 | maxIndex: React.PropTypes.number.isRequired,
16 | title: React.PropTypes.string.isRequired,
17 | description: React.PropTypes.string.isRequired,
18 | valid: React.PropTypes.bool.isRequired,
19 | checkingOutput: React.PropTypes.bool.isRequired,
20 | rules: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
21 | required: React.PropTypes.arrayOf(React.PropTypes.bool).isRequired,
22 | present: React.PropTypes.arrayOf(React.PropTypes.bool).isRequired,
23 | unmount: React.PropTypes.func.isRequired,
24 | successText: React.PropTypes.oneOfType([
25 | React.PropTypes.string,
26 | React.PropTypes.object,
27 | ]),
28 | },
29 |
30 | challengeSuccess() {
31 | var code = this.refs.editor.getText();
32 | challengeCompleted(code);
33 | },
34 |
35 | render() {
36 | var {
37 | courseCompleted,
38 | index,
39 | maxIndex,
40 | title,
41 | description,
42 | valid,
43 | checkingOutput,
44 | rules,
45 | required,
46 | present,
47 | unmount,
48 | successText,
49 | } = this.props;
50 |
51 | // true if the `required` and `present` arrays match perfectly
52 | var isCorrect = required.every((x, i) => x === present[i]);
53 |
54 | return (
55 |
56 |
${index + 1} of ${maxIndex + 1}` : '') + title
62 | }}
63 | />
64 |
67 |
68 | {courseCompleted ?
:
69 |
84 | }
85 |
91 |
92 |
93 | );
94 | },
95 | });
96 |
97 | module.exports = Challenge;
98 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var $ = require('gulp-load-plugins')();
3 | var source = require('vinyl-source-stream');
4 | var buffer = require('vinyl-buffer');
5 | var browserify = require('browserify');
6 | var to5ify = require('6to5ify');
7 | var paths = require('./paths.js');
8 |
9 | var argv = process.argv;
10 | var PROD = argv[argv.length - 1] === '--prod' || argv[2] === 'deploy';
11 |
12 | process.env.NODE_ENV = PROD ? 'production' : 'development';
13 |
14 | gulp.task('lint', function () {
15 | return gulp.src(paths.client.scripts.all)
16 | .pipe($.jshint())
17 | .pipe($.jshint.reporter('default'));
18 | });
19 |
20 | gulp.task('demo_lint', function () {
21 | return gulp.src(paths.demo.scripts.all)
22 | .pipe($.jshint())
23 | .pipe($.jshint.reporter('default'));
24 | });
25 |
26 | var bundler =
27 | browserify(paths.client.scripts.entry, {
28 | debug: !PROD,
29 | standalone: 'challenger',
30 | });
31 |
32 | var demo_bundler =
33 | browserify(paths.demo.scripts.entry, {
34 | debug: !PROD,
35 | });
36 |
37 | gulp.task('scripts', function() {
38 | return bundler.transform(to5ify)
39 | .bundle()
40 | .on('error', $.util.log.bind($.util, 'Browserify Error: Client'))
41 | .pipe(source('challenger.min.js'))
42 | .pipe(buffer())
43 | // if we're in production, uglify it
44 | .pipe($.if(PROD, $.uglify()))
45 | // maybe hook up sourcemaps at some point...
46 | //.pipe($.if(PROD, $.sourcemaps.init({loadMaps: true})))
47 | //.pipe($.if(PROD, $.sourcemaps.write('./')))
48 | .pipe(gulp.dest(paths.client.dist))
49 | .pipe(gulp.dest(paths.demo.dist));
50 | });
51 |
52 | gulp.task('demo_scripts', ['demo_lint'], function() {
53 | return demo_bundler.transform(to5ify)
54 | .bundle()
55 | .on('error', $.util.log.bind($.util, 'Browserify Error: Demo'))
56 | .pipe(source('demo.js'))
57 | .pipe(buffer())
58 | .pipe(gulp.dest(paths.demo.dist));
59 | });
60 |
61 | gulp.task('sass', function () {
62 | return gulp.src(paths.client.stylesheets.entry)
63 | .pipe($.sass({ indentedSyntax: true }))
64 | .pipe($.autoprefixer({ browsers: [
65 | 'ie >= 9',
66 | 'ff >= 9',
67 | 'Chrome >= 18',
68 | 'Opera >= 15',
69 | 'Safari >= 5.1',
70 | '> 1%',
71 | 'last 2 versions',
72 | 'Firefox ESR'
73 | ]}))
74 | .pipe(gulp.dest(paths.client.temp));
75 | });
76 |
77 | gulp.task('demo_sass', function () {
78 | return gulp.src(paths.demo.stylesheets.entry)
79 | .pipe($.sass({ indentedSyntax: true }))
80 | .pipe($.autoprefixer({
81 | browsers: ['ie >= 8', '> 1%', 'last 2 versions', 'Firefox ESR', 'Opera 12.1']
82 | }))
83 | .pipe(gulp.dest(paths.demo.dist));
84 | });
85 |
86 | gulp.task('css_concat', ['sass'], function () {
87 | return gulp.src([paths.client.temp + '*.css'].concat(paths.client.stylesheets.plugins))
88 | .pipe($.concat('challenger.min.css'))
89 | .pipe($.if(PROD, $.minifyCss()))
90 | .pipe(gulp.dest(paths.client.dist))
91 | .pipe(gulp.dest(paths.demo.dist));
92 | });
93 |
94 | gulp.task('static', function () {
95 | return gulp.src(paths.client.static.all, { dot: true })
96 | .pipe(gulp.dest(paths.client.dist));
97 | });
98 |
99 | gulp.task('demo_static', function () {
100 | return gulp.src(paths.demo.static.all, { dot: true })
101 | .pipe(gulp.dest(paths.demo.dist));
102 | });
103 |
104 | gulp.task('watch', function () {
105 | gulp.watch([paths.client.scripts.all, paths.shared.scripts.all], [/*'lint', 'test',*/ 'scripts']);
106 | gulp.watch([paths.client.stylesheets.all], ['css_concat']);
107 | gulp.watch([paths.demo.scripts.all], ['demo_scripts']);
108 | gulp.watch([paths.demo.stylesheets.all], ['demo_sass']);
109 | gulp.watch([paths.demo.static.dir], ['demo_static']);
110 | });
111 |
112 | gulp.task('webserver', function () {
113 | return gulp.src(paths.demo.dist)
114 | .pipe($.webserver({
115 | host: 'localhost',
116 | livereload: true,
117 | open: true
118 | }));
119 | });
120 |
121 | gulp.task('test', ['scripts'], function () {
122 | return gulp.src(paths.tests.entry, {read: false})
123 | .pipe($.mocha({reporter: 'nyan'}));
124 | });
125 |
126 | gulp.task( 'build', ['scripts', 'css_concat', 'static']);
127 | gulp.task( 'demo', ['demo_scripts', 'demo_sass', 'demo_static']);
128 | gulp.task( 'default', [ 'build', 'demo', 'watch', 'webserver' ] );
129 |
130 | gulp.task('deploy', ['build', 'demo'], function () {
131 | return gulp.src(paths.demo.dist + '/**/*', { dot: true })
132 | .pipe($.ghPages('https://github.com/rileyjshaw/challenger.git', 'origin'));
133 | });
134 |
--------------------------------------------------------------------------------
/client/src/scripts/stores/challenge.js:
--------------------------------------------------------------------------------
1 | // holds current user input and compares it with
2 | // a ruleset from courseStore.
3 | //
4 | // reflux
5 | var Reflux = require('reflux');
6 | var actions = require('../actions');
7 | var courseStore = require('./course');
8 |
9 | // acorn
10 | var acorn = require('acorn');
11 | var walkAST = require('acorn/dist/walk').ancestor;
12 |
13 | // utils
14 | var categorizeChains = require('../util/categorizeChains');
15 | var chainMatch = require('../util/chainMatch');
16 | var fillArray = require('../util/fillArray');
17 | var runCode = require('../util/runCode.js');
18 |
19 | var challengeStore = Reflux.createStore({
20 | listenables: actions,
21 | init() {
22 | // stored values
23 | this.numRules = 0;
24 | this.initialCode = '';
25 | this.present = [];
26 | this.customRules = [];
27 | this.nestedRules = [];
28 | this.walkableNestedRules = {};
29 | this.debounceTimeout = null;
30 | this.clearLastExecution = () => null;
31 | this.output = undefined;
32 |
33 | // register courseStore's changes
34 | this.listenTo(courseStore, this.updateRuleset);
35 | },
36 |
37 | updateRuleset({rules, numRules, initialCode}) {
38 | // course store also sends out a maxIndex trigger; only run on ruleset triggers
39 | if (numRules) {
40 | // add `index` key to rule array to keep position reference
41 | // in filtered lists
42 | rules = rules.map((rule, i) => {
43 | rule.index = i;
44 | return rule;
45 | });
46 |
47 | this.numRules = numRules;
48 | this.initialCode = initialCode;
49 | this.present = fillArray(numRules, false);
50 | this.customRules = rules.filter(rule => rule.type === 'custom');
51 | this.nestedRules = rules.filter(rule => rule.type === 'expressionChain');
52 | this.walkableNestedRules = this.createWalkableRuleset(this.nestedRules);
53 | this.debounceTimeout = null;
54 | this.clearLastExecution = () => null;
55 | this.output = rules.filter(rule => rule.type === 'output')[0];
56 | }
57 | },
58 |
59 | // handle codeEditUser action, called from Editor.
60 | // verifyStructure, verifyCustomRules, and verifyOutput
61 | // all have side effects on the this.present array,
62 | // updating it for the current input.
63 | onCodeEditUser(input) {
64 | // the following three methods have side effects on `this.present`
65 | var valid = this.verifyNestedRules(input);
66 | var checkingOutput = valid && this.output;
67 | this.verifyCustomRules(input);
68 |
69 | // debounce runCode function so that it's only run after
70 | // typing has paused for 450ms
71 | clearTimeout(this.debounceTimeout);
72 |
73 | // stop the last worker from running
74 | this.clearLastExecution();
75 |
76 | if (checkingOutput) {
77 | // sync, set output to false before running
78 | this.present[this.output.index] = false;
79 |
80 | // async, updates output rule
81 | this.debounceTimeout = setTimeout(() => {
82 | this.clearLastExecution = this.verifyOutput(input);
83 | }, 450);
84 | }
85 |
86 | this.triggerPresent(valid, checkingOutput); // sync, updates structure & custom rules
87 | },
88 |
89 | // edits code and overwrites it in CodeMirror
90 | onCodeEditOverride(input) {
91 | // state will be set on the resultant onCodeEditUser action
92 | this.trigger(input || this.initialCode);
93 | },
94 |
95 | verifyNestedRules(input) {
96 | // reset each nestedRule index in this.present to false
97 | this.nestedRules.forEach(({index}) => this.present[index] = false);
98 |
99 | // `walkAST` sets this.present to true for matching rules
100 | try {
101 | let ast = acorn.parse(input, { ecmaVersion: 6 });
102 | walkAST(ast, this.walkableNestedRules);
103 | return true;
104 | } catch (err) {
105 | return false;
106 | }
107 | },
108 |
109 | verifyCustomRules(input) {
110 | this.customRules.forEach(({verify, index}) => {
111 | this.present[index] = !!verify(input);
112 | });
113 | },
114 |
115 | verifyOutput(input) {
116 | var {index, setup, teardown, verify} = this.output;
117 |
118 | var fullCode = `${setup};${input};${teardown}`;
119 | var trigger = (present) => {
120 | this.present[index] = present;
121 | this.triggerPresent(true, false);
122 | };
123 |
124 | return runCode(fullCode, verify, trigger);
125 | },
126 |
127 | // returns an obj that can be used by acorn's ancestor walk
128 | createWalkableRuleset(rules) {
129 | var categorized = categorizeChains(rules);
130 |
131 | return Object.keys(categorized).reduce((walkableRuleset, exp) => {
132 | // an array of {index, chain} objects
133 | var rules = categorized[exp];
134 |
135 | walkableRuleset[exp] = (node, state) => {
136 | // cycle through each rule chain when a node
137 | // of a given type is reached
138 | rules.forEach(({index, chain}) => {
139 | if (chainMatch(chain, state)) {
140 | this.present[index] = true;
141 | }
142 | });
143 | };
144 |
145 | return walkableRuleset;
146 | }, {});
147 | },
148 |
149 | triggerPresent(valid, checkingOutput) {
150 | this.trigger({
151 | // send a new object, since pureRenderMixin compares pointers
152 | present: this.present.slice(),
153 | valid: !!valid,
154 | checkingOutput: !!checkingOutput
155 | });
156 | }
157 | });
158 |
159 | module.exports = challengeStore;
160 |
--------------------------------------------------------------------------------
/client/dist/operative.min.js:
--------------------------------------------------------------------------------
1 | /** Operative v0.4.4 (c) 2013 James padolsey, MIT-licensed, http://github.com/padolsey/operative **/
2 | (function(){function e(t,n){var o=e.getBaseURL,i=e.getSelfURL,s=e.hasWorkerSupport?e.Operative.BrowserWorker:e.Operative.Iframe;if("function"==typeof t){var a=new s({main:t},n,o,i),u=function(){return a.api.main.apply(a,arguments)};u.transfer=function(){return a.api.main.transfer.apply(a,arguments)};for(var l in a.api)r.call(a.api,l)&&(u[l]=a.api[l]);return u}return new s(t,n,o,i).api}if("undefined"!=typeof window||!self.importScripts){var r={}.hasOwnProperty,t=document.getElementsByTagName("script"),n=t[t.length-1],o=/operative/.test(n.src)&&n.src;e.pool=function(r,t,n){r=0|Math.abs(r)||1;for(var o=[],i=0,s=0;r>s;++s)o.push(e(t,n));return{terminate:function(){for(var e=0;r>e;++e)o[e].destroy()},next:function(){return i=i+1===r?0:i+1,o[i]}}},e.hasWorkerSupport=!!window.Worker,e.hasWorkerViaBlobSupport=!1,e.hasTransferSupport=!1;var i=(location.protocol+"//"+location.hostname+(location.port?":"+location.port:"")+location.pathname).replace(/[^\/]+$/,"");e.objCreate=Object.create||function(e){function r(){}return r.prototype=e,new r},e.setSelfURL=function(e){o=e},e.getSelfURL=function(){return o},e.setBaseURL=function(e){i=e},e.getBaseURL=function(){return i},window.operative=e}})(),function(){function e(e){this.value=e}function r(e,r,n,o){var i=this;e.get=e.get||function(e){return this[e]},e.set=e.set||function(e,r){return this[e]=r},this._curToken=0,this._queue=[],this._getBaseURL=n,this._getSelfURL=o,this.isDestroyed=!1,this.isContextReady=!1,this.module=e,this.dependencies=r||[],this.dataProperties={},this.api={},this.callbacks={},this.deferreds={},this._fixDependencyURLs(),this._setup();for(var s in e)t.call(e,s)&&this._createExposedMethod(s);this.api.__operative__=this,this.api.destroy=this.api.terminate=function(){return i.destroy()}}if("undefined"!=typeof window||!self.importScripts){var t={}.hasOwnProperty,n=[].slice,o={}.toString;operative.Operative=r;var i=r.Promise=window.Promise;r.prototype={_marshal:function(e){return e},_demarshal:function(e){return e},_enqueue:function(e){this._queue.push(e)},_fixDependencyURLs:function(){for(var e=this.dependencies,r=0,t=e.length;t>r;++r){var n=e[r];/\/\//.test(n)||(e[r]=n.replace(/^\/?/,this._getBaseURL().replace(/([^\/])$/,"$1/")))}},_dequeueAll:function(){for(var e=0,r=this._queue.length;r>e;++e)this._queue[e].call(this);this._queue=[]},_buildContextScript:function(e){var r,t=[],n=this.module,o=this.dataProperties;for(var i in n)r=n[i],"function"==typeof r?t.push(' self["'+i.replace(/"/g,'\\"')+'"] = '+(""+r)+";"):o[i]=r;return t.join("\n")+(e?"\n("+(""+e)+"());":"")},_createExposedMethod:function(r){var t=this,s=this.api[r]=function(){function o(){t.isContextReady?t._runMethod(r,s,a,l):t._enqueue(o)}if(t.isDestroyed)throw Error("Operative: Cannot run method. Operative has already been destroyed");var s=++t._curToken,a=n.call(arguments),u="function"==typeof a[a.length-1]&&a.pop(),l=a[a.length-1]instanceof e&&a.pop();if(!u&&!i)throw Error("Operative: No callback has been passed. Assumed that you want a promise. But `operative.Promise` is null. Please provide Promise polyfill/lib.");if(u)t.callbacks[s]=u,setTimeout(function(){o()},1);else if(i)return new i(function(e,r){var n;e.fulfil||e.fulfill?(n=e,n.fulfil=n.fulfill=e.fulfil||e.fulfill):n={fulfil:e,fulfill:e,resolve:e,reject:r,transferResolve:e,transferReject:r},t.deferreds[s]=n,o()})};s.transfer=function(){var r=[].slice.call(arguments),t="function"==typeof r[r.length-1]?r.length-2:r.length-1,n=r[t],i=o.call(n);if("[object Array]"!==i)throw Error("Operative:transfer() must be passed an Array of transfers as its last arguments (Expected: [object Array], Received: "+i+")");return r[t]=new e(n),s.apply(null,r)}},destroy:function(){this.isDestroyed=!0}}}}(),function(){function makeBlobURI(e){var r;try{r=new Blob([e],{type:"text/javascript"})}catch(t){r=new BlobBuilder,r.append(e),r=r.getBlob()}return URL.createObjectURL(r)}function workerBoilerScript(){var postMessage=self.postMessage,structuredCloningSupport=null,toString={}.toString;self.console={},self.isWorker=!0,["log","debug","error","info","warn","time","timeEnd"].forEach(function(e){self.console[e]=function(){postMessage({cmd:"console",method:e,args:[].slice.call(arguments)})}}),self.addEventListener("message",function(e){function callback(){returnResult({args:[].slice.call(arguments)})}function returnResult(e,r){postMessage({cmd:"result",token:data.token,result:e},hasTransferSupport&&r||[])}function extractTransfers(e){var r=e[e.length-1];if("[object Array]"!==toString.call(r))throw Error("Operative: callback.transfer() must be passed an Array of transfers as its last arguments");return r}var data=e.data;if("string"==typeof data&&0===data.indexOf("EVAL|"))return eval(data.substring(5)),void 0;if(null==structuredCloningSupport)return structuredCloningSupport="PING"===e.data[0],self.postMessage(structuredCloningSupport?"pingback:structuredCloningSupport=YES":"pingback:structuredCloningSupport=NO"),structuredCloningSupport||(postMessage=function(e){return self.postMessage(JSON.stringify(e))}),void 0;structuredCloningSupport||(data=JSON.parse(data));var defs=data.definitions,isDeferred=!1,args=data.args;if(defs)for(var i in defs)self[i]=defs[i];else{callback.transfer=function(){var e=[].slice.call(arguments),r=extractTransfers(e);returnResult({args:e},r)},args.push(callback),self.deferred=function(){function e(e,r){return returnResult({isDeferred:!0,action:"resolve",args:[e]},r),t}function r(e,r){returnResult({isDeferred:!0,action:"reject",args:[e]},r)}isDeferred=!0;var t={};return t.fulfil=t.fulfill=t.resolve=function(r){return e(r)},t.reject=function(e){return r(e)},t.transferResolve=function(r){var t=extractTransfers(arguments);return e(r,t)},t.transferReject=function(e){var t=extractTransfers(arguments);return r(e,t)},t};var result=self[data.method].apply(self,args);isDeferred||void 0===result||returnResult({args:[result]}),self.deferred=function(){throw Error("Operative: deferred() called at odd time")}}})}if("undefined"==typeof window&&self.importScripts)return workerBoilerScript(),void 0;var Operative=operative.Operative,URL=window.URL||window.webkitURL,BlobBuilder=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder,workerViaBlobSupport=function(){try{new Worker(makeBlobURI(";"))}catch(e){return!1}return!0}(),transferrableObjSupport=function(){try{var e=new ArrayBuffer(1);return new Worker(makeBlobURI(";")).postMessage(e,[e]),!e.byteLength}catch(r){return!1}}();operative.hasWorkerViaBlobSupport=workerViaBlobSupport,operative.hasTransferSupport=transferrableObjSupport,Operative.BrowserWorker=function BrowserWorker(){Operative.apply(this,arguments)};var WorkerProto=Operative.BrowserWorker.prototype=operative.objCreate(Operative.prototype);WorkerProto._onWorkerMessage=function(e){var r=e.data;if("string"==typeof r&&0===r.indexOf("pingback"))return"pingback:structuredCloningSupport=NO"===r&&(this._marshal=function(e){return JSON.stringify(e)},this._demarshal=function(e){return JSON.parse(e)}),this.isContextReady=!0,this._postMessage({definitions:this.dataProperties}),this._dequeueAll(),void 0;switch(r=this._demarshal(r),r.cmd){case"console":window.console&&window.console[r.method].apply(window.console,r.args);break;case"result":var t=this.callbacks[r.token],n=this.deferreds[r.token],o=r.result&&r.result.isDeferred&&r.result.action;n&&o?n[o](r.result.args[0]):t?t.apply(this,r.result.args):n&&n.fulfil(r.result.args[0])}},WorkerProto._isWorkerViaBlobSupported=function(){return workerViaBlobSupport},WorkerProto._setup=function(){var e,r=this,t=this._getSelfURL(),n=this._isWorkerViaBlobSupported(),o=this._buildContextScript(n?workerBoilerScript:"");if(this.dependencies.length&&(o='importScripts("'+this.dependencies.join('", "')+'");\n'+o),n)e=this.worker=new Worker(makeBlobURI(o));else{if(!t)throw Error("Operaritve: No operative.js URL available. Please set via operative.setSelfURL(...)");e=this.worker=new Worker(t),e.postMessage("EVAL|"+o)}e.postMessage("EVAL|self.hasTransferSupport="+transferrableObjSupport),e.postMessage(["PING"]),e.addEventListener("message",function(e){r._onWorkerMessage(e)})},WorkerProto._postMessage=function(e){var r=transferrableObjSupport&&e.transfers;return r?this.worker.postMessage(e,r.value):this.worker.postMessage(this._marshal(e))},WorkerProto._runMethod=function(e,r,t,n){this._postMessage({method:e,args:t,token:r,transfers:n})},WorkerProto.destroy=function(){this.worker.terminate(),Operative.prototype.destroy.call(this)}}(),function(){function e(){window.__run__=function(e,r,t,n){function o(){return t.apply(this,arguments)}var i=!1;window.deferred=function(){return i=!0,n},o.transfer=function(){return t.apply(this,[].slice.call(arguments,0,arguments.length-1))},t&&r.push(o);var s=window[e].apply(window,r);window.deferred=function(){throw Error("Operative: deferred() called at odd time")},i||void 0===s||o(s)}}if("undefined"!=typeof window||!self.importScripts){var r=operative.Operative;r.Iframe=function(){r.apply(this,arguments)};var t=r.Iframe.prototype=operative.objCreate(r.prototype),n=0;t._setup=function(){var r=this,t="__operativeIFrameLoaded"+ ++n;this.module.isWorker=!1;var o=this.iframe=document.body.appendChild(document.createElement("iframe"));o.style.display="none";var i=this.iframeWindow=o.contentWindow,s=i.document;window[t]=function(){window[t]=null;var n=s.createElement("script"),o=r._buildContextScript(e);void 0!==n.text?n.text=o:n.innerHTML=o,s.documentElement.appendChild(n);for(var a in r.dataProperties)i[a]=r.dataProperties[a];r.isContextReady=!0,r._dequeueAll()},s.open();var a="";this.dependencies.length&&(a+='\n'),s.write(a+"\n"),s.close()},t._runMethod=function(e,r,t){var n=this,o=this.callbacks[r],i=this.deferreds[r];this.iframeWindow.__run__(e,t,function(e){var r=o,t=i;r?r.apply(n,arguments):t&&t.fulfil(e)},i)},t.destroy=function(){this.iframe.parentNode.removeChild(this.iframe),r.prototype.destroy.call(this)}}}();
--------------------------------------------------------------------------------
/client/dist/challenger.min.css:
--------------------------------------------------------------------------------
1 | .challenger .challenge-outer::before,.cm-tab-wrap-hack:after{content:''}@font-face{font-family:Inconsolata;font-style:normal;font-weight:400;src:local('Inconsolata'),url(http://fonts.gstatic.com/s/inconsolata/v10/BjAYBlHtW3CJxDcjzrnZCJ0EAVxt0G0biEntp43Qt6E.ttf)format('truetype')}@font-face{font-family:Roboto;font-style:normal;font-weight:400;src:local('Roboto'),local('Roboto-Regular'),url(http://fonts.gstatic.com/s/roboto/v15/zN7GBFwfMP4uA6AR0HCoLQ.ttf)format('truetype')}@font-face{font-family:Roboto;font-style:normal;font-weight:700;src:local('Roboto Bold'),local('Roboto-Bold'),url(http://fonts.gstatic.com/s/roboto/v15/d-6IYplOFocCacKzxwXSOKCWcynf_cDxXwCLxiixG1c.ttf)format('truetype')}@-webkit-keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes rotate{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate{100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dash{0%{stroke-dasharray:30%,240%;stroke-dashoffset:0}50%{stroke-dasharray:240%,30%;stroke-dashoffset:0}100%{stroke-dasharray:30%,240%;stroke-dashoffset:-270%}}@-moz-keyframes dash{0%{stroke-dasharray:30%,240%;stroke-dashoffset:0}50%{stroke-dasharray:240%,30%;stroke-dashoffset:0}100%{stroke-dasharray:30%,240%;stroke-dashoffset:-270%}}@keyframes dash{0%{stroke-dasharray:30%,240%;stroke-dashoffset:0}50%{stroke-dasharray:240%,30%;stroke-dashoffset:0}100%{stroke-dasharray:30%,240%;stroke-dashoffset:-270%}}.challenger{position:fixed;top:0;bottom:0;left:0;right:0;overflow-x:hidden;color:#212121;background:rgba(102,77,171,.69)}.challenger,.challenger button{font:16px Roboto,Helvetica,'Helvetica Neue',Arial,sans-serif}.challenger .spinner{position:absolute;top:0;left:0;display:none;height:100%;width:100%}.challenger .spinner svg{position:absolute;top:50%;left:50%;-webkit-animation:rotate 2s linear infinite;-moz-animation:rotate 2s linear infinite;animation:rotate 2s linear infinite}.challenger .spinner circle{stroke:#fff;fill:none;-webkit-animation:dash 1.5s ease-in-out infinite;-moz-animation:dash 1.5s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite;stroke-linecap:round}.challenger .spinning .spinner{display:block}.challenger .challenge-outer{height:100%;width:100%;white-space:nowrap;text-align:center}.challenger .challenge-outer::before{display:inline-block;height:100%;vertical-align:middle}.challenger .challenge{position:relative;display:inline-block;width:90%;max-width:1280px;vertical-align:middle;white-space:normal;text-align:left;background:#fff;box-shadow:0 1px 6px rgba(0,0,0,.12);-webkit-transition:left 1s;-moz-transition:left 1s;transition:left 1s}.challenger .challenge h1{line-height:60px;margin:0;padding:0 60px 0 30px;color:#e8cfff;background:#664dab}.challenger .challenge h1 small{line-height:.5em;margin-right:18px;font-size:.5em}.challenger .challenge-frame{padding:18px 30px}.challenger .challenge-content{position:relative;margin-bottom:18px}.challenger .challenge-content>*{width:48%}.challenger .description{margin-top:0;font-size:22px;line-height:1.46em}.challenger .description code{padding:0 .2em}.challenger .rules{position:absolute;right:0;bottom:0;max-height:100%;overflow-y:auto;margin:0;padding:0;list-style:none}.challenger .rules li{position:relative;margin-bottom:12px;padding:1.6em 1em;box-shadow:0 1px 6px rgba(0,0,0,.12)}.challenger .rules li p{margin:0}.challenger .rules li:last-child{margin-bottom:0}.challenger .CodeMirror,.challenger code{font-family:Inconsolata,monospace}.challenger .CodeMirror{-moz-box-sizing:border-box;box-sizing:border-box;border:1px solid #212121}.challenger .cm-s-neo .CodeMirror-gutters{background:#fff}.challenger .unmount{position:absolute;top:12px;right:12px;height:36px;width:36px;padding:0;border:0;background:0 0;cursor:pointer}.challenger .unmount:hover line{stroke:#fff!important}.challenger .submit{height:48px;width:100%;padding:.6em 1em;border:0;border-radius:0;color:#0a634d;background:#1de9b6;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.24)}.challenger .submit[disabled]{color:#595e75;background:#ccced8;cursor:default}.challenger .successScreen{text-align:center}.challenger .successScreen .textBefore{margin-bottom:24px;font-size:36px}.challenger .successScreen svg{height:240px;width:240px;margin:24px 0}.challenger .successScreen .textAfter{margin-bottom:1.5em;font-size:24px}.challenger .complete{color:#0a634d;background:#1de9b6}.challenger .blocked{color:#afb2c2;background:#ccced8}.challenger .incomplete{color:#801f00;background:#ff784d}@media screen and (max-width:920px){.challenger{background:#fff}.challenger .challenge{width:100%;box-shadow:none}.challenger .challenge-frame{padding:18px 3%}.challenger .challenge-content>*{width:100%}.challenger .rules{position:static;max-height:none;margin-bottom:30px}}.challenger .challenge-enter{left:100%}.challenger .challenge-enter.challenge-enter-active{left:0}.challenger .challenge-leave{position:absolute;top:50%;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%);left:0}.challenger .challenge-leave.challenge-leave-active{left:-100%}.CodeMirror{font-family:monospace;height:300px;color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror div.CodeMirror-cursor{border-left:1px solid #000}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.CodeMirror.cm-fat-cursor div.CodeMirror-cursor{width:auto;border:0;background:#7e7}.CodeMirror.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1)infinite;-moz-animation:blink 1.06s steps(1)infinite;animation:blink 1.06s steps(1)infinite}@-moz-keyframes blink{0%,100%{background:#7e7}50%{background:0 0}}@-webkit-keyframes blink{0%,100%{background:#7e7}50%{background:0 0}}@keyframes blink{0%,100%{background:#7e7}50%{background:0 0}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-measure pre{position:static}.CodeMirror div.CodeMirror-cursor{position:absolute;border-right:none;width:0}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror ::selection,.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror ::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}span.CodeMirror-selectedtext{background:0 0}.cm-s-neo.CodeMirror{background-color:#fff;color:#2e383c;line-height:1.4375}.cm-s-neo .cm-comment{color:#75787b}.cm-s-neo .cm-keyword,.cm-s-neo .cm-property{color:#1d75b3}.cm-s-neo .cm-atom,.cm-s-neo .cm-number{color:#75438a}.cm-s-neo .cm-node,.cm-s-neo .cm-tag{color:#9c3328}.cm-s-neo .cm-string{color:#b35e14}.cm-s-neo .cm-qualifier,.cm-s-neo .cm-variable{color:#047d65}.cm-s-neo pre{padding:0}.cm-s-neo .CodeMirror-gutters{border:none;border-right:10px solid transparent;background-color:transparent}.cm-s-neo .CodeMirror-linenumber{padding:0;color:#e0e2e5}.cm-s-neo .CodeMirror-guttermarker{color:#1d75b3}.cm-s-neo .CodeMirror-guttermarker-subtle{color:#e0e2e5}.cm-s-neo div.CodeMirror-cursor{width:auto;border:0;background:rgba(155,157,162,.37);z-index:1}.CodeMirror-lint-markers{width:16px}.CodeMirror-lint-tooltip{background-color:infobackground;border:1px solid #000;border-radius:4px;color:infotext;font-family:monospace;font-size:10pt;overflow:hidden;padding:2px 5px;position:fixed;white-space:pre;white-space:pre-wrap;z-index:100;max-width:600px;opacity:0;transition:opacity .4s;-moz-transition:opacity .4s;-webkit-transition:opacity .4s;-o-transition:opacity .4s;-ms-transition:opacity .4s}.CodeMirror-lint-mark-error,.CodeMirror-lint-mark-warning{background-position:left bottom;background-repeat:repeat-x}.CodeMirror-lint-mark-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==)}.CodeMirror-lint-mark-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-error,.CodeMirror-lint-marker-warning{background-position:center center;background-repeat:no-repeat;cursor:pointer;display:inline-block;height:16px;width:16px;vertical-align:middle;position:relative}.CodeMirror-lint-message-error,.CodeMirror-lint-message-warning{padding-left:18px;background-position:top left;background-repeat:no-repeat}.CodeMirror-lint-marker-error,.CodeMirror-lint-message-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=)}.CodeMirror-lint-marker-warning,.CodeMirror-lint-message-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-multiple{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:right bottom;width:100%;height:100%}
--------------------------------------------------------------------------------