Add loop-protect

Remove webworkers
This commit is contained in:
Berkeley Martinez
2015-11-30 14:27:39 -08:00
parent 063d16383f
commit 3a299daa37
15 changed files with 597 additions and 513 deletions

View File

@ -1,24 +0,0 @@
window.common = (function(global) {
const {
common = { init: [] }
} = global;
const faux$ = common.getScriptContent$('/js/faux.js').shareReplay();
common.hasJs = function hasJs(code = '') {
return code.match(/\<\s?script\s?\>/gi) &&
code.match(/\<\s?\/\s?script\s?\>/gi);
};
common.addFaux$ = function addFaux$(code) {
// grab user javaScript
var scriptCode = code
.split(/\<\s?script\s?\>/gi)[1]
.split(/\<\s?\/\s?script\s?\>/gi)[0];
return faux$.map(faux => faux + scriptCode);
};
return common;
}(window));

View File

@ -0,0 +1,17 @@
window.common = (function(global) {
const {
loopProtect,
common = { init: [] }
} = global;
loopProtect.hit = function hit(line) {
var err = `Error: Exiting potential infinite loop at line ${line}.`;
console.error(err);
};
common.addLoopProtect = function addLoopProtect(code = '') {
return loopProtect(code);
};
return common;
})(window);

View File

@ -1,38 +0,0 @@
window.common = (function({ common = { init: [] }}) {
var BDDregex = new RegExp(
'(expect(\\s+)?\\(.*\\;)|' +
'(assert(\\s+)?\\(.*\\;)|' +
'(assert\\.\\w.*\\;)|' +
'(.*\\.should\\..*\\;)/'
);
common.addTestsToString = function({ code, tests = [], ...rest }) {
const userTests = [];
code = tests.reduce((code, test) => code + test + '\n', code + '\n');
var counter = 0;
var match = BDDregex.exec(code);
while (match) {
var replacement = '//' + counter + common.salt;
code = code.substring(0, match.index) +
replacement +
code.substring(match.index + match[0].length);
userTests.push({
err: null,
text: match[0],
line: counter
});
counter++;
match = BDDregex.exec(code);
}
return { ...rest, code, userTests };
};
return common;
}(window));

View File

@ -1,79 +0,0 @@
window.common = (function(global) {
const {
jailed,
document: doc,
Rx: { Observable, Disposable },
common = { init: [] }
} = global;
if (!jailed) {
return (code, cb) => cb(new Error('Could not load jailed plugin'));
}
// obtaining absolute path of this script
var scripts = doc.getElementsByTagName('script');
var path = scripts[scripts.length - 1].src
.split('?')[0]
.split('/')
.slice(0, -1)
.join('/') + '/';
var Sandbox = {
startTimeout() {
this.timeoutId = setTimeout(() => {
this.error = new Error('Plugin failed to initialize');
this.destroyPlugin();
}, 3000);
},
cancelTimout() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
},
createPlugin() {
this.plugin = new jailed.Plugin(path + 'plugin.js');
this.startTimeout();
this.plugin.whenConnected(() => {
this.cancelTimout();
});
},
destroyPlugin() {
this.plugin.disconnect();
}
};
// sends the input to the plugin for evaluation
common.detectLoops$ = function detectLoops$({ code = '', ...rest }) {
return Observable.create(function(observer) {
const sandbox = Object.create(Sandbox);
sandbox.createPlugin();
sandbox.plugin.whenConnected(() => {
sandbox.plugin.remote.run(code, (err, data) => {
observer.onNext({ ...rest, err, code, data });
observer.onCompleted();
});
});
sandbox.plugin.whenDisconnected(() => {
if (sandbox.disposed) {
return null;
}
if (sandbox.error) {
observer.onNext({ ...rest, err: sandbox.error, code, data: {} });
}
observer.onCompleted();
});
return new Disposable(() => {
sandbox.disposed = true;
sandbox.destroyPlugin();
});
});
};
return common;
}(window));

View File

@ -28,18 +28,7 @@ $(document).ready(function() {
// only run for HTML
.filter(() => common.challengeType === challengeTypes.HTML)
.flatMap(code => {
if (
common.hasJs(code)
) {
return common.detectUnsafeCode$(code)
.flatMap(code => common.detectLoops$(code))
.flatMap(
({ err }) => err ? Observable.throw(err) : Observable.just(code)
)
.flatMap(code => common.updatePreview$(code))
.catch(err => Observable.just({ err }));
}
return Observable.just(code)
.flatMap(code => common.updatePreview$(code))
.catch(err => Observable.just({ err }));
})
@ -66,12 +55,12 @@ $(document).ready(function() {
.catch(err => Observable.just({ err }));
})
.subscribe(
({ err, output, original }) => {
({ err, output, originalCode }) => {
if (err) {
console.error(err);
return common.updateOutputDisplay('' + err);
}
common.codeStorage.updateStorage(challengeName, original);
common.codeStorage.updateStorage(challengeName, originalCode);
common.updateOutputDisplay('' + output);
},
(err) => {
@ -102,11 +91,11 @@ $(document).ready(function() {
if (common.challengeType === common.challengeTypes.HTML) {
return common.updatePreview$(`
<h1>${err}</h1>
`).subscribe(() => {});
`).first().subscribe(() => {});
}
return common.updateOutputDisplay('' + err);
}
common.updateOutputDisplay(output);
common.updateOutputDisplay('' + output);
common.displayTestResults(tests);
if (solved) {
common.showCompletion();
@ -153,12 +142,12 @@ $(document).ready(function() {
.flatMap(() => common.executeChallenge$())
.catch(err => Observable.just({ err }))
.subscribe(
({ err, original, tests }) => {
({ err, originalCode, tests }) => {
if (err) {
console.error(err);
return common.updateOutputDisplay('' + err);
}
common.codeStorage.updateStorage(challengeName, original);
common.codeStorage.updateStorage(challengeName, originalCode);
common.displayTestResults(tests);
},
(err) => {

View File

@ -1,16 +1,17 @@
// what are the executeChallenge functions?
// Should be responsible for starting after a submit action
// Should not be responsible for displaying results
// Should return results
// should grab editor value
// depends on main editor
window.common = (function(global) {
const {
ga,
Rx: { Observable },
common = { init: [] }
} = global;
const {
addLoopProtect,
detectUnsafeCode$,
updatePreview$,
challengeType,
challengeTypes
} = common;
let attempts = 0;
common.executeChallenge$ = function executeChallenge$() {
@ -18,67 +19,39 @@ window.common = (function(global) {
const originalCode = code;
const head = common.arrayToNewLineString(common.head);
const tail = common.arrayToNewLineString(common.tail);
const combinedCode = head + code + tail;
attempts++;
ga('send', 'event', 'Challenge', 'ran-code', common.challengeName);
// run checks for unsafe code
return common.detectUnsafeCode$(code)
return detectUnsafeCode$(code)
// add head and tail and detect loops
.map(code => head + code + tail)
.map(() => {
if (challengeType !== challengeTypes.HTML) {
return `<script>;${addLoopProtect(combinedCode)}/**/</script>`;
}
return addLoopProtect(combinedCode);
})
.flatMap(code => updatePreview$(code))
.flatMap(code => {
if (common.challengeType === common.challengeTypes.HTML) {
let output;
if (common.hasJs(code)) {
// html has a script code
// add faux code and test in webworker
return common.addFaux$(code)
.flatMap(code => common.detectLoops$(code))
.flatMap(({ err }) => {
if (err) {
return Observable.throw(err);
}
return common.updatePreview$(code)
.flatMap(() => common.runPreviewTests$({
code,
tests: common.tests.slice()
}));
});
if (
challengeType === challengeTypes.HTML &&
common.hasJs(code)
) {
output = common.getJsOutput(common.getJsFromHtml(code));
} else if (challengeType !== challengeTypes.HTML) {
output = common.getJsOutput(combinedCode);
}
// no script code detected in html code
// Update preview and run tests in iframe
return common.updatePreview$(code)
.flatMap(code => common.runPreviewTests$({
code,
tests: common.tests.slice()
}));
}
// js challenge
// remove comments and add tests to string
return Observable.just(common.addTestsToString(Object.assign(
{
code: common.removeComments(code),
tests: common.tests.slice()
}
)))
.flatMap(common.detectLoops$)
.flatMap(({ err, code, data, userTests }) => {
if (err) {
return Observable.throw(err);
}
// run tests
// for now these are running in the browser
return common.runTests$({
data,
code,
userTests,
return common.runPreviewTests$({
tests: common.tests.slice(),
originalCode,
output: data.output.replace(/\\\"/gi, '')
});
output
});
});
};

View File

@ -0,0 +1,23 @@
window.common = (function(global) {
const {
common = { init: [] },
document: doc
} = global;
common.getIframe = function getIframe(id = 'preview') {
let previewFrame = doc.getElementById(id);
// create and append a hidden preview frame
if (!previewFrame) {
previewFrame = doc.createElement('iframe');
previewFrame.id = id;
previewFrame.setAttribute('style', 'display: none');
doc.body.appendChild(previewFrame);
}
return previewFrame.contentDocument ||
previewFrame.contentWindow.document;
};
return common;
})(window);

View File

@ -102,5 +102,19 @@ window.common = (function(global) {
});
};
const openScript = /\<\s?script\s?\>/gi;
const closingScript = /\<\s?\/\s?script\s?\>/gi;
// detects if there is JavaScript in the first script tag
common.hasJs = function hasJs(code) {
return !!common.getJsFromHtml(code);
};
// grabs the content from the first script tag in the code
common.getJsFromHtml = function getJsFromHtml(code) {
// grab user javaScript
return (code.split(openScript)[1] || '').split(closingScript)[0] || '';
};
return common;
})(window);

View File

@ -4,7 +4,13 @@ window.common = (function(global) {
common = { init: [] }
} = global;
// the first script tag here is to proxy jQuery
// We use the same jQuery on the main window but we change the
// context to that of the iframe.
var libraryIncludes = `
<script>
window.$ = parent.$.proxy(parent.$.fn.find, parent.$(document));
</script>
<link
rel='stylesheet'
href='//cdnjs.cloudflare.com/ajax/libs/animate.css/3.2.0/animate.min.css'
@ -33,15 +39,10 @@ window.common = (function(global) {
// runPreviewTests$ should be set up in the preview window
common.runPreviewTests$ =
() => Observable.throw({ err: new Error('run preview not enabled') });
() => Observable.throw(new Error('run preview not enabled'));
common.updatePreview$ = function updatePreview$(code = '') {
const previewFrame = document.getElementById('preview');
const preview = previewFrame.contentDocument ||
previewFrame.contentWindow.document;
if (!preview) {
return Observable.just(code);
}
const preview = common.getIframe('preview');
return iFrameScript$
.map(script => `<script>${script}</script>`)
@ -55,7 +56,10 @@ window.common = (function(global) {
// now we filter false values and wait for the first true
return common.previewReady$
.filter(ready => ready)
.first();
.first()
// the delay here is to give code within the iframe
// control to run
.delay(100);
})
.map(() => code);
};

View File

@ -1,275 +0,0 @@
/* eslint-disable no-unused-vars */
var document = {};
var navigator = function() {
this.geolocation = function() {
this.getCurrentPosition = function() {
this.coords = {latitude: '', longitude: '' };
return this;
};
return this;
};
return this;
};
function $() {
if (!(this instanceof $)) {
return new $();
}
}
function returnThis() { return this; }
function return$() { return $; }
var methods = [
'add',
'addBack',
'addClass',
'after',
'ajaxComplete',
'ajaxError',
'ajaxSend',
'ajaxStart',
'ajaxStop',
'ajaxSuccess',
'andSelf',
'animate',
'append',
'appendTo',
'attr',
'before',
'bind',
'blur',
'callbacksadd',
'callbacksdisable',
'callbacksdisabled',
'callbacksempty',
'callbacksfire',
'callbacksfired',
'callbacksfireWith',
'callbackshas',
'callbackslock',
'callbackslocked',
'callbacksremove',
'change',
'children',
'clearQueue',
'click',
'clone',
'closest',
'contents',
'context',
'css',
'data',
'dblclick',
'delay',
'delegate',
'dequeue',
'detach',
'die',
'each',
'empty',
'end',
'eq',
'error',
'fadeIn',
'fadeOut',
'fadeTo',
'fadeToggle',
'filter',
'find',
'finish',
'first',
'focus',
'focusin',
'focusout',
'get',
'has',
'hasClass',
'height',
'hide',
'hover',
'html',
'index',
'innerHeight',
'innerWidth',
'insertAfter',
'insertBefore',
'is',
'jQuery',
'jquery',
'keydown',
'keypress',
'keyup',
'last',
'length',
'live',
'load',
'load',
'map',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
'next',
'nextAll',
'nextUntil',
'not',
'off',
'offset',
'offsetParent',
'on',
'one',
'outerHeight',
'outerWidth',
'parent',
'parents',
'parentsUntil',
'position',
'prepend',
'prependTo',
'prev',
'prevAll',
'prevUntil',
'promise',
'prop',
'pushStack',
'queue',
'ready',
'remove',
'removeAttr',
'removeClass',
'removeData',
'removeProp',
'replaceAll',
'replaceWith',
'resize',
'scroll',
'scrollLeft',
'scrollTop',
'select',
'selector',
'serialize',
'serializeArray',
'show',
'siblings',
'size',
'slice',
'slideDown',
'slideToggle',
'slideUp',
'stop',
'submit',
'text',
'toArray',
'toggle',
'toggle',
'toggleClass',
'trigger',
'triggerHandler',
'unbind',
'undelegate',
'unload',
'unwrap',
'val',
'width',
'wrap',
'wrapAll',
'wrapInner'
];
var statics = [
'ajax',
'ajaxPrefilter',
'ajaxSetup',
'ajaxTransport',
'boxModel',
'browser',
'Callbacks',
'contains',
'cssHooks',
'cssNumber',
'data',
'Deferred',
'dequeue',
'each',
'error',
'extend',
'fnextend',
'fxinterval',
'fxoff',
'get',
'getJSON',
'getScript',
'globalEval',
'grep',
'hasData',
'holdReady',
'inArray',
'isArray',
'isEmptyObject',
'isFunction',
'isNumeric',
'isPlainObject',
'isWindow',
'isXMLDoc',
'makeArray',
'map',
'merge',
'noConflict',
'noop',
'now',
'param',
'parseHTML',
'parseJSON',
'parseXML',
'post',
'proxy',
'queue',
'removeData',
'sub',
'support',
'trim',
'type',
'unique',
'when',
'always',
'done',
'fail',
'isRejected',
'isResolved',
'notify',
'notifyWith',
'pipe',
'progress',
'promise',
'reject',
'rejectWith',
'resolve',
'resolveWith',
'state',
'then',
'currentTarget',
'data',
'delegateTarget',
'isDefaultPrevented',
'isImmediatePropagationStopped',
'isPropagationStopped',
'metaKey',
'namespace',
'pageX',
'pageY',
'preventDefault',
'relatedTarget',
'result',
'stopImmediatePropagation',
'stopPropagation',
'target',
'timeStamp',
'type',
'which'
];
var $Proto = {};
methods.forEach(method => $Proto[method] = returnThis);
statics.forEach(staticMeth => $[staticMeth] = return$);
$.prototype = Object.create($);

View File

@ -1,21 +1,43 @@
/* eslint-disable no-undef, no-unused-vars, no-native-reassign */
window.__$ = parent.$;
window.__$(function() {
// the $ on the iframe window object is the same
// as the one used on the main site, but
// uses the iframe document as the context
window.$(document).ready(function() {
var _ = parent._;
var Rx = parent.Rx;
var chai = parent.chai;
var assert = chai.assert;
var tests = parent.tests;
var common = parent.common;
var editor = common.editor.getValue();
// change the context of $ so it uses the iFrame for testing
var $ = __$.proxy(__$.fn.find, __$(document));
common.getJsOutput = function evalJs(code = '') {
let output;
try {
/* eslint-disable no-eval */
output = eval(code);
/* eslint-enable no-eval */
} catch (e) {
window.__err = e;
}
return output;
};
common.runPreviewTests$ =
function runPreviewTests$({ tests = [], ...rest }) {
function runPreviewTests$({
tests = [],
originalCode,
...rest
}) {
const code = originalCode;
const editor = { getValue() { return originalCode; } };
if (window.__err) {
return Rx.Observable.throw(window.__err);
}
return Rx.Observable.from(tests)
.map(test => {
const userTest = {};
common.appendToOutputDisplay('');
try {
/* eslint-disable no-eval */
eval(test);
@ -32,12 +54,13 @@ window.__$(function() {
return userTest;
})
.toArray()
.map(tests => ({ ...rest, tests }));
.map(tests => ({ ...rest, tests, originalCode }));
};
// now that the runPreviewTest$ is defined
// we set the subject to true
// this will let the updatePreview
// script now that we are ready.
console.log('second');
common.previewReady$.onNext(true);
});

View File

@ -18,6 +18,7 @@ var Rx = require('rx'),
uglify = require('gulp-uglify'),
merge = require('merge-stream'),
babel = require('gulp-babel'),
sourcemaps = require('gulp-sourcemaps'),
// react app
webpack = require('webpack-stream'),
@ -85,7 +86,8 @@ var paths = {
'public/bower_components/CodeMirror/mode/xml/xml.js',
'public/bower_components/CodeMirror/mode/css/css.js',
'public/bower_components/CodeMirror/mode/htmlmixed/htmlmixed.js',
'node_modules/emmet-codemirror/dist/emmet.js'
'node_modules/emmet-codemirror/dist/emmet.js',
'public/js/lib/loop-protect/loop-protect.js'
],
vendorMain: [
@ -103,20 +105,20 @@ var paths = {
js: [
'client/main.js',
'client/iFrameScripts.js',
'client/plugin.js',
'client/faux.js'
'client/plugin.js'
],
commonFramework: [
'init',
'bindings',
'add-test-to-string',
'add-faux-stream',
'code-storage',
'code-uri',
'add-loop-protect',
'get-iframe',
'update-preview',
'create-editor',
'detect-unsafe-code-stream',
'detect-loops-stream',
'display-test-results',
'execute-challenge-stream',
'output-display',
@ -125,7 +127,6 @@ var paths = {
'run-tests-stream',
'show-completion',
'step-challenge',
'update-preview',
'end'
],
@ -434,7 +435,9 @@ gulp.task('dependents', ['js'], function() {
return gulp.src(formatCommonFrameworkPaths.call(paths.commonFramework))
.pipe(plumber({ errorHandler: errorHandler }))
.pipe(babel())
.pipe(__DEV__ ? sourcemaps.init() : gutil.noop())
.pipe(concat('commonFramework.js'))
.pipe(__DEV__ ? sourcemaps.write() : gutil.noop())
.pipe(__DEV__ ? gutil.noop() : uglify())
.pipe(revReplace({ manifest: manifest }))
.pipe(gulp.dest(dest))

View File

@ -134,6 +134,7 @@
"browser-sync": "^2.9.12",
"chai": "^3.4.0",
"envify": "^3.4.0",
"gulp-sourcemaps": "^1.6.0",
"istanbul": "~0.4.0",
"jsonlint": "^1.6.2",
"loopback-component-explorer": "^2.1.1",

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 JS Bin Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,432 @@
if (typeof DEBUG === 'undefined') { DEBUG = true; }
(function (root, factory) {
'use strict';
/*global define*/
if (typeof define === 'function' && define.amd) {
define(factory(root));
} else if (typeof exports === 'object') {
module.exports = factory(root);
} else {
root.loopProtect = factory(root);
}
})(this, function loopProtectModule(root) {
/*global DEBUG*/
'use strict';
var debug = null;
// the standard loops - note that recursive is not supported
var re = /\b(for|while|do)\b/g;
var reSingle = /\b(for|while|do)\b/;
var labelRe = /\b([a-z_]{1}\w+:)/i;
var comments = /(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s;])+\/\/(?:.*)$)/gm;
var loopProtect = rewriteLoops;
// used in the loop detection
loopProtect.counters = {};
// expose debug info
loopProtect.debug = function debugSwitch(state) {
debug = state ? function () {
console.log.apply(console, [].slice.apply(arguments));
} : function () {};
};
loopProtect.debug(false); // off by default
// the method - as this could be aliased to something else
loopProtect.alias = 'loopProtect';
function inMultilineComment(lineNum, lines) {
if (lineNum === 0) {
return false;
}
var j = lineNum;
var closeCommentTags = 1; // let's assume we're inside a comment
var closePos = -1;
var openPos = -1;
do {
j -= 1;
DEBUG && debug('looking backwards ' + lines[j]); // jshint ignore:line
closePos = lines[j].indexOf('*/');
openPos = lines[j].indexOf('/*');
if (closePos !== -1) {
closeCommentTags++;
}
if (openPos !== -1) {
closeCommentTags--;
if (closeCommentTags === 0) {
DEBUG && debug('- exit: part of a multiline comment'); // jshint ignore:line
return true;
}
}
} while (j !== 0);
return false;
}
function inCommentOrString(index, line) {
var character;
while (--index > -1) {
character = line.substr(index, 1);
if (character === '"' || character === '\'' || character === '.') {
// our loop keyword was actually either in a string or a property, so let's exit and ignore this line
DEBUG && debug('- exit: matched inside a string or property key'); // jshint ignore:line
return true;
}
if (character === '/' || character === '*') {
// looks like a comment, go back one to confirm or not
--index;
if (character === '/') {
// we've found a comment, so let's exit and ignore this line
DEBUG && debug('- exit: part of a comment'); // jshint ignore:line
return true;
}
}
}
return false;
}
function directlyBeforeLoop(index, lineNum, lines) {
reSingle.lastIndex = 0;
labelRe.lastIndex = 0;
var beforeLoop = false;
var theRest = lines.slice(lineNum).join('\n').substr(index).replace(labelRe, '');
theRest.replace(reSingle, function commentStripper(match, capture, i) {
var target = theRest.substr(0, i).replace(comments, '').trim();
DEBUG && debug('- directlyBeforeLoop: ' + target); // jshint ignore:line
if (target.length === 0) {
beforeLoop = true;
}
// strip comments out of the target, and if there's nothing else
// it's a valid label...I hope!
});
return beforeLoop;
}
/**
* Look for for, while and do loops, and inserts *just* at the start of the
* loop, a check function.
*/
function rewriteLoops(code, offset) {
var recompiled = [];
var lines = code.split('\n');
var disableLoopProtection = false;
var method = loopProtect.alias + '.protect';
var ignore = {};
var pushonly = {};
var labelPostion = null;
function insertReset(lineNum, line, matchPosition) {
// recompile the line with the reset **just** before the actual loop
// so that we insert in to the correct location (instead of possibly
// outside the logic
return line.slice(0, matchPosition) + ';' + method + '({ line: ' + lineNum + ', reset: true }); ' + line.slice(matchPosition);
};
if (!offset) {
offset = 0;
}
lines.forEach(function eachLine(line, lineNum) {
// reset our regexp each time.
re.lastIndex = 0;
labelRe.lastIndex = 0;
if (disableLoopProtection) {
return;
}
if (line.toLowerCase().indexOf('noprotect') !== -1) {
disableLoopProtection = true;
}
var index = -1;
var matchPosition = -1;
var originalLineNum = lineNum;
// +1 since we're humans and don't read lines numbers from zero
var printLineNumber = lineNum - offset + 1;
var character = '';
// special case for `do` loops, as they're end with `while`
var dofound = false;
var findwhile = false;
var terminator = false;
var matches = line.match(re) || [];
var match = matches.length ? matches[0] : '';
var labelMatch = line.match(labelRe) || [];
var openBrackets = 0;
var openBraces = 0;
if (labelMatch.length) {
DEBUG && debug('- label match'); // jshint ignore:line
index = line.indexOf(labelMatch[1]);
if (!inCommentOrString(index, line)) {
if (!inMultilineComment(lineNum, lines)) {
if (directlyBeforeLoop(index, lineNum, lines)) {
DEBUG && debug('- found a label: "' + labelMatch[0] + '"'); // jshint ignore:line
labelPostion = lineNum;
} else {
DEBUG && debug('- ignored "label", false positive'); // jshint ignore:line
}
} else {
DEBUG && debug('- ignored label in multline comment'); // jshint ignore:line
}
} else {
DEBUG && debug('- ignored label in string or comment'); // jshint ignore:line
}
}
if (ignore[lineNum]) {
DEBUG && debug(' -exit: ignoring line ' + lineNum +': ' + line); // jshint ignore:line
return;
}
if (pushonly[lineNum]) {
DEBUG && debug('- exit: ignoring, but adding line ' + lineNum + ': ' + line); // jshint ignore:line
recompiled.push(line);
return;
}
// if there's more than one match, we just ignore this kind of loop
// otherwise I'm going to be writing a full JavaScript lexer...and god
// knows I've got better things to be doing.
if (match && matches.length === 1 && line.indexOf('jsbin') === -1) {
DEBUG && debug('match on ' + match + '\n'); // jshint ignore:line
// there's a special case for protecting `do` loops, we need to first
// prtect the `do`, but then ignore the closing `while` statement, so
// we reset the search state for this special case.
dofound = match === 'do';
// make sure this is an actual loop command by searching backwards
// to ensure it's not a string, comment or object property
matchPosition = index = line.indexOf(match);
// first we need to walk backwards to ensure that our match isn't part
// of a string or part of a comment
if (inCommentOrString(index, line)) {
recompiled.push(line);
return;
}
// it's quite possible we're in the middle of a multiline
// comment, so we'll cycle up looking for an opening comment,
// and if there's one (and not a closing `*/`), then we'll
// ignore this line as a comment
if (inMultilineComment(lineNum, lines)) {
recompiled.push(line);
return;
}
// now work our way forward to look for '{'
index = line.indexOf(match) + match.length;
if (index === line.length) {
if (index === line.length && lineNum < (lines.length-1)) {
// move to the next line
DEBUG && debug('- moving to next line'); // jshint ignore:line
recompiled.push(line);
lineNum++;
line = lines[lineNum];
ignore[lineNum] = true;
index = 0;
}
}
while (index < line.length) {
character = line.substr(index, 1);
// DEBUG && debug(character, index); // jshint ignore:line
if (character === '(') {
openBrackets++;
}
if (character === ')') {
openBrackets--;
if (openBrackets === 0 && terminator === false) {
terminator = index;
}
}
if (character === '{') {
openBraces++;
}
if (character === '}') {
openBraces--;
}
if (openBrackets === 0 && (character === ';' || character === '{')) {
// if we're a non-curlies loop, then convert to curlies to get our code inserted
if (character === ';') {
if (lineNum !== originalLineNum) {
DEBUG && debug('- multiline inline loop'); // jshint ignore:line
// affect the compiled line
recompiled[originalLineNum] = recompiled[originalLineNum].substring(0, terminator + 1) + '{\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n' + recompiled[originalLineNum].substring(terminator + 1);
line += '\n}\n';
} else {
// simpler
DEBUG && debug('- single line inline loop'); // jshint ignore:line
line = line.substring(0, terminator + 1) + '{\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n' + line.substring(terminator + 1) + '\n}\n';
}
} else if (character === '{') {
DEBUG && debug('- multiline with braces'); // jshint ignore:line
var insert = ';\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n';
line = line.substring(0, index + 1) + insert + line.substring(index + 1);
index += insert.length;
}
// work out where to put the reset
if (lineNum === originalLineNum && labelPostion === null) {
DEBUG && debug('- simple reset insert'); // jshint ignore:line
line = insertReset(printLineNumber, line, matchPosition);
index += (';' + method + '({ line: ' + lineNum + ', reset: true }); ').length;
} else {
// insert the reset above the originalLineNum OR if this loop used
// a label, we have to insert the reset *above* the label
if (labelPostion === null) {
DEBUG && debug('- reset inserted above original line'); // jshint ignore:line
recompiled[originalLineNum] = insertReset(printLineNumber, recompiled[originalLineNum], matchPosition);
} else {
DEBUG && debug('- reset inserted above matched label on line ' + labelPostion); // jshint ignore:line
if (recompiled[labelPostion] === undefined) {
labelPostion--;
matchPosition = 0;
}
recompiled[labelPostion] = insertReset(printLineNumber, recompiled[labelPostion], matchPosition);
labelPostion = null;
}
}
recompiled.push(line);
if (!dofound) {
return;
} else {
DEBUG && debug('searching for closing `while` statement for: ' + line); // jshint ignore:line
// cycle forward until we find the close brace, after which should
// be our while statement to ignore
findwhile = false;
while (index < line.length) {
character = line.substr(index, 1);
if (character === '{') {
openBraces++;
}
if (character === '}') {
openBraces--;
}
if (openBraces === 0) {
findwhile = true;
} else {
findwhile = false;
}
if (openBraces === 0) {
DEBUG && debug('outside of closure, looking for `while` statement: ' + line); // jshint ignore:line
}
if (findwhile && line.indexOf('while') !== -1) {
DEBUG && debug('- exit as we found `while`: ' + line); // jshint ignore:line
pushonly[lineNum] = true;
return;
}
index++;
if (index === line.length && lineNum < (lines.length-1)) {
lineNum++;
line = lines[lineNum];
DEBUG && debug(line); // jshint ignore:line
index = 0;
}
}
return;
}
}
index++;
if (index === line.length && lineNum < (lines.length-1)) {
// move to the next line
DEBUG && debug('- moving to next line'); // jshint ignore:line
recompiled.push(line);
lineNum++;
line = lines[lineNum];
ignore[lineNum] = true;
index = 0;
}
}
} else {
// else we're a regular line, and we shouldn't be touched
DEBUG && debug('regular line ' + line); // jshint ignore:line
recompiled.push(line);
}
});
DEBUG && debug('---- source ----'); // jshint ignore:line
DEBUG && debug(code); // jshint ignore:line
DEBUG && debug('---- rewrite ---'); // jshint ignore:line
DEBUG && debug(recompiled.join('\n')); // jshint ignore:line
DEBUG && debug(''); // jshint ignore:line
return disableLoopProtection ? code : recompiled.join('\n');
};
/**
* Injected code in to user's code to **try** to protect against infinite
* loops cropping up in the code, and killing the browser. Returns true
* when the loops has been running for more than 100ms.
*/
loopProtect.protect = function protect(state) {
loopProtect.counters[state.line] = loopProtect.counters[state.line] || {};
var line = loopProtect.counters[state.line];
var now = (new Date()).getTime();
if (state.reset) {
line.time = now;
line.hit = 0;
line.last = 0;
}
line.hit++;
if ((now - line.time) > 100) {//} && line.hit !== line.last+1) {
// We've spent over 100ms on this loop... smells infinite.
loopProtect.hit(state.line);
// Returning true prevents the loop running again
return true;
}
line.last++;
return false;
};
loopProtect.hit = function hit(line) {
var msg = 'Exiting potential infinite loop at line ' + line + '. To disable loop protection: add "// noprotect" to your code';
if (root.proxyConsole) {
root.proxyConsole.error(msg);
} else {
console.error(msg);
}
};
loopProtect.reset = function reset() {
// reset the counters
loopProtect.counters = {};
};
return loopProtect;
});