├── .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 | 16 | 22 | 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 | 24 | 25 | 26 | 27 | 28 | 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 |
      45 | { rules } 46 |
    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 |