diff --git a/src/js/config/Categories.js b/src/js/config/Categories.js
index 2611f300..14efda69 100755
--- a/src/js/config/Categories.js
+++ b/src/js/config/Categories.js
@@ -287,6 +287,7 @@ var Categories = [
"Jump",
"Conditional Jump",
"Return",
+ "Wait",
]
},
];
diff --git a/src/js/config/OperationConfig.js b/src/js/config/OperationConfig.js
index b0dfc735..fb212e72 100755
--- a/src/js/config/OperationConfig.js
+++ b/src/js/config/OperationConfig.js
@@ -3120,5 +3120,19 @@ var OperationConfig = {
outputType: "html",
args: [
]
- }
+ },
+ "Wait": {
+ description: "Waits for a number of milliseconds.",
+ run: FlowControl.runWait,
+ inputType: "string",
+ outputType: "string",
+ flowControl: true,
+ args: [
+ {
+ name: "Sleep time in milliseconds",
+ type: "number",
+ value: FlowControl.SLEEP_TIME,
+ }
+ ]
+ },
};
diff --git a/src/js/core/Chef.js b/src/js/core/Chef.js
index e6bac71d..c10796db 100755
--- a/src/js/core/Chef.js
+++ b/src/js/core/Chef.js
@@ -34,7 +34,7 @@ Chef.prototype.bake = function(inputText, recipeConfig, options, progress, step)
var startTime = new Date().getTime(),
recipe = new Recipe(recipeConfig),
containsFc = recipe.containsFlowControl(),
- error = false;
+ chef = this;
// Reset attemptHighlight flag
if (options.hasOwnProperty("attemptHighlight")) {
@@ -64,28 +64,41 @@ Chef.prototype.bake = function(inputText, recipeConfig, options, progress, step)
// If starting from scratch, load data
if (progress === 0) {
- this.dish.set(inputText, Dish.STRING);
+ chef.dish.set(inputText, Dish.STRING);
}
- try {
- progress = recipe.execute(this.dish, progress);
- } catch (err) {
- // Return the error in the result so that everything else gets correctly updated
- // rather than throwing it here and losing state info.
- error = err;
- progress = err.progress;
- }
-
- return {
- result: this.dish.type === Dish.HTML ?
- this.dish.get(Dish.HTML) :
- this.dish.get(Dish.STRING),
- type: Dish.enumLookup(this.dish.type),
- progress: progress,
+ var ret = {
options: options,
- duration: new Date().getTime() - startTime,
- error: error
+ error: false,
};
+
+ return new Promise(function(resolve) {
+ recipe.execute(chef.dish, progress)
+ .then(function(progress) {
+ ret.result = chef.dish.type === Dish.HTML ?
+ chef.dish.get(Dish.HTML) :
+ chef.dish.get(Dish.STRING);
+ ret.type = Dish.enumLookup(chef.dish.type);
+
+ ret.duration = new Date().getTime() - startTime;
+ ret.progress = progress;
+
+ resolve(ret);
+ })
+ .catch(function(err) {
+ ret.result = chef.dish.type === Dish.HTML ?
+ chef.dish.get(Dish.HTML) :
+ chef.dish.get(Dish.STRING);
+ ret.type = Dish.enumLookup(chef.dish.type);
+
+ ret.duration = new Date().getTime() - startTime;
+ ret.progress = err.progress;
+ ret.error = err;
+
+ // Resolve not reject: we are packaging the error as a value.
+ resolve(ret);
+ });
+ });
};
diff --git a/src/js/core/FlowControl.js b/src/js/core/FlowControl.js
index 6f56be66..340736f4 100755
--- a/src/js/core/FlowControl.js
+++ b/src/js/core/FlowControl.js
@@ -59,29 +59,53 @@ var FlowControl = {
}
}
- var recipe = new Recipe(),
- output = "",
- progress = 0;
-
+ var recipe = new Recipe();
recipe.addOperations(subOpList);
- // Run recipe over each tranche
- for (i = 0; i < inputs.length; i++) {
- var dish = new Dish(inputs[i], inputType);
- try {
- progress = recipe.execute(dish, 0);
- } catch (err) {
- if (!ignoreErrors) {
- throw err;
- }
- progress = err.progress + 1;
- }
- output += dish.get(outputType) + mergeDelim;
- }
+ return new Promise(function(resolve, reject) {
+ var promises = inputs.map(function(input, i) {
+ var forkDish = new Dish(input, inputType);
- state.dish.set(output, outputType);
- state.progress += progress;
- return state;
+ return new Promise(function(resolve, reject) {
+ recipe.execute(forkDish, 0)
+ .then(function(progress) {
+ resolve({
+ progress: progress,
+ dish: forkDish,
+ });
+ })
+ .catch(function(err) {
+ if (ignoreErrors) {
+ resolve({
+ progress: err.progress + 1,
+ dish: forkDish,
+ });
+ } else {
+ reject(err);
+ }
+ });
+ });
+
+ });
+
+ Promise.all(promises)
+ .then(function(values) {
+ var progress;
+
+ var output = values.map(function(value) {
+ progress = value.progress;
+ return value.dish.get(outputType);
+ }).join(mergeDelim);
+
+ state.progress += progress;
+ state.dish.set(output, outputType);
+
+ resolve(state);
+ })
+ .catch(function(err) {
+ reject(err);
+ });
+ });
},
@@ -183,4 +207,32 @@ var FlowControl = {
return state;
},
+
+ /**
+ * @constant
+ * @default
+ */
+ SLEEP_TIME: 2500,
+
+
+ /**
+ * Wait operation.
+ *
+ * @param {Object} state - The current state of the recipe.
+ * @param {number} state.progress - The current position in the recipe.
+ * @param {Dish} state.dish - The Dish being operated on.
+ * @param {Operation[]} state.opList - The list of operations in the recipe.
+ * @returns {Object} The updated state of the recipe.
+ */
+ runWait: function(state) {
+ var ings = state.opList[state.progress].getIngValues(),
+ sleepTime = ings[0];
+
+ return new Promise(function(resolve, reject) {
+ setTimeout(function() {
+ resolve(state);
+ }, sleepTime);
+ });
+ },
+
};
diff --git a/src/js/core/Recipe.js b/src/js/core/Recipe.js
index d5f383fc..5a8cadf3 100755
--- a/src/js/core/Recipe.js
+++ b/src/js/core/Recipe.js
@@ -138,58 +138,112 @@ Recipe.prototype.lastOpIndex = function(startIndex) {
* Executes each operation in the recipe over the given Dish.
*
* @param {Dish} dish
- * @param {number} [startFrom=0] - The index of the Operation to start executing from
+ * @param {number} [currentStep=0] - The index of the Operation to start executing from
* @returns {number} - The final progress through the recipe
*/
-Recipe.prototype.execute = function(dish, startFrom) {
- startFrom = startFrom || 0;
- var op, input, output, numJumps = 0;
+Recipe.prototype.execute = function(dish, currentStep, state) {
+ var recipe = this;
- for (var i = startFrom; i < this.opList.length; i++) {
- op = this.opList[i];
- if (op.isDisabled()) {
- continue;
- }
- if (op.isBreakpoint()) {
- return i;
+ var formatErrMsg = function(err, step, op) {
+ var e = typeof err == "string" ? { message: err } : err;
+
+ e.progress = step;
+ if (e.fileName) {
+ e.displayStr = op.name + " - " + e.name + " in " +
+ e.fileName + " on line " + e.lineNumber +
+ ".
Message: " + (e.displayStr || e.message);
+ } else {
+ e.displayStr = op.name + " - " + (e.displayStr || e.message);
}
- try {
+ return e;
+ };
+
+ // Operations can be asynchronous so we have to return a Promise to a
+ // future value.
+ return new Promise(function(resolve, reject) {
+ // Helper function to clean up recursing to the next recipe step.
+ // It is a closure to avoid having to pass in resolve and reject.
+ var execRecipe = function(recipe, dish, step, state) {
+ return recipe.execute(dish, step, state)
+ .then(function(progress) {
+ resolve(progress);
+ })
+ .catch(function(err) {
+ // Pass back the error to the previous caller.
+ // We don't want to handle the error here as the current
+ // operation did not cause the error, and so it should
+ // not appear in the error message.
+ reject(err);
+ });
+ };
+
+ currentStep = currentStep || 0;
+
+ if (currentStep === recipe.opList.length) {
+ resolve(currentStep);
+ return;
+ }
+
+ var op = recipe.opList[currentStep],
input = dish.get(op.inputType);
+ if (op.isDisabled()) {
+ // Skip to next operation
+ var nextStep = currentStep + 1;
+ execRecipe(recipe, dish, nextStep, state);
+ } else if (op.isBreakpoint()) {
+ // We are at a breakpoint, we shouldn't recurse to the next op.
+ resolve(currentStep);
+ } else {
+ var operationResult;
+
+ // We must try/catch here because op.run can either return
+ // A) a value
+ // B) a promise
+ // Promise.resolve -> .catch will handle errors from promises
+ // try/catch will handle errors from values
+ try {
+ if (op.isFlowControl()) {
+ state = {
+ progress: currentStep,
+ dish: dish,
+ opList: recipe.opList,
+ numJumps: (state && state.numJumps) || 0,
+ };
+ operationResult = op.run(state);
+ } else {
+ operationResult = op.run(input, op.getIngValues());
+ }
+ } catch (err) {
+ reject(formatErrMsg(err, currentStep, op));
+ return;
+ }
+
if (op.isFlowControl()) {
- // Package up the current state
- var state = {
- "progress" : i,
- "dish" : dish,
- "opList" : this.opList,
- "numJumps" : numJumps
- };
-
- state = op.run(state);
- i = state.progress;
- numJumps = state.numJumps;
+ Promise.resolve(operationResult)
+ .then(function(state) {
+ return recipe.execute(state.dish, state.progress + 1);
+ })
+ .then(function(progress) {
+ resolve(progress);
+ })
+ .catch(function(err) {
+ reject(formatErrMsg(err, currentStep, op));
+ });
} else {
- output = op.run(input, op.getIngValues());
- dish.set(output, op.outputType);
+ Promise.resolve(operationResult)
+ .then(function(output) {
+ dish.set(output, op.outputType);
+ var nextStep = currentStep + 1;
+ execRecipe(recipe, dish, nextStep, state);
+ })
+ .catch(function(err) {
+ reject(formatErrMsg(err, currentStep, op));
+ });
}
- } catch (err) {
- var e = typeof err == "string" ? { message: err } : err;
-
- e.progress = i;
- if (e.fileName) {
- e.displayStr = op.name + " - " + e.name + " in " +
- e.fileName + " on line " + e.lineNumber +
- ".
Message: " + (e.displayStr || e.message);
- } else {
- e.displayStr = op.name + " - " + (e.displayStr || e.message);
- }
-
- throw e;
}
- }
-
- return this.opList.length;
+ });
};
diff --git a/src/js/views/html/HTMLApp.js b/src/js/views/html/HTMLApp.js
index 4e7aa78d..604fe449 100755
--- a/src/js/views/html/HTMLApp.js
+++ b/src/js/views/html/HTMLApp.js
@@ -28,6 +28,9 @@ var HTMLApp = function(categories, operations, defaultFavourites, defaultOptions
this.progress = 0;
this.ingId = 0;
+ this.baking = false;
+ this.rebake = false;
+
window.chef = this.chef;
};
@@ -61,6 +64,36 @@ HTMLApp.prototype.handleError = function(err) {
};
+/**
+ * Updates the UI to show if baking is in process or not.
+ *
+ * @param {bakingStatus}
+ */
+HTMLApp.prototype.setBakingStatus = function(bakingStatus) {
+ var inputLoadingIcon = document.querySelector("#input .title .loading-icon");
+ var outputLoadingIcon = document.querySelector("#output .title .loading-icon");
+
+ var inputElement = document.querySelector("#input-text");
+ var outputElement = document.querySelector("#output-text");
+
+ if (bakingStatus) {
+ inputLoadingIcon.style.display = "inline-block";
+ outputLoadingIcon.style.display = "inline-block";
+ inputElement.classList.add("disabled");
+ outputElement.classList.add("disabled");
+ inputElement.disabled = true;
+ outputElement.disabled = true;
+ } else {
+ inputLoadingIcon.style.display = "none";
+ outputLoadingIcon.style.display = "none";
+ inputElement.classList.remove("disabled");
+ outputElement.classList.remove("disabled");
+ inputElement.disabled = false;
+ outputElement.disabled = false;
+ }
+};
+
+
/**
* Calls the Chef to bake the current input using the current recipe.
*
@@ -68,37 +101,70 @@ HTMLApp.prototype.handleError = function(err) {
* whole recipe.
*/
HTMLApp.prototype.bake = function(step) {
- var response;
+ var app = this;
+
+ if (app.baking) {
+ if (!app.rebake) {
+ // We do not want to keep autobaking
+ // Say that we will rebake and then try again later
+ app.rebake = true;
+ setTimeout(function() {
+ app.bake(step);
+ }, 500);
+ }
+
+ return;
+ }
+
+ app.rebake = false;
+ app.baking = true;
+ app.setBakingStatus(true);
try {
- response = this.chef.bake(
- this.getInput(), // The user's input
- this.getRecipeConfig(), // The configuration of the recipe
- this.options, // Options set by the user
- this.progress, // The current position in the recipe
+ app.chef.bake(
+ app.getInput(), // The user's input
+ app.getRecipeConfig(), // The configuration of the recipe
+ app.options, // Options set by the user
+ app.progress, // The current position in the recipe
step // Whether or not to take one step or execute the whole recipe
- );
+ )
+ .then(function(response) {
+ app.baking = false;
+ app.setBakingStatus(false);
+
+ if (!response) {
+ return;
+ }
+ if (response.error) {
+ app.handleError(response.error);
+ }
+
+ app.options = response.options;
+
+ if (response.type === "html") {
+ app.dishStr = Utils.stripHtmlTags(response.result, true);
+ } else {
+ app.dishStr = response.result;
+ }
+
+ app.progress = response.progress;
+ app.manager.recipe.updateBreakpointIndicator(response.progress);
+ app.manager.output.set(response.result, response.type, response.duration);
+
+ // If baking took too long, disable auto-bake
+ if (response.duration > app.options.autoBakeThreshold && app.autoBake_) {
+ app.manager.controls.setAutoBake(false);
+ app.alert("Baking took longer than " + app.options.autoBakeThreshold +
+ "ms, Auto Bake has been disabled.", "warning", 5000);
+ }
+ })
+ .catch(function(err) {
+ console.error("Chef's promise was rejected, should never occur");
+ });
} catch (err) {
- this.handleError(err);
- }
-
- if (!response) return;
-
- if (response.error) {
- this.handleError(response.error);
- }
-
- this.options = response.options;
- this.dishStr = response.type === "html" ? Utils.stripHtmlTags(response.result, true) : response.result;
- this.progress = response.progress;
- this.manager.recipe.updateBreakpointIndicator(response.progress);
- this.manager.output.set(response.result, response.type, response.duration);
-
- // If baking took too long, disable auto-bake
- if (response.duration > this.options.autoBakeThreshold && this.autoBake_) {
- this.manager.controls.setAutoBake(false);
- this.alert("Baking took longer than " + this.options.autoBakeThreshold +
- "ms, Auto Bake has been disabled.", "warning", 5000);
+ app.baking = false;
+ app.setBakingStatus(false);
+ app.handleError(err);
}
};
diff --git a/src/static/stats.txt b/src/static/stats.txt
index 87a5337f..dfbf5578 100644
--- a/src/static/stats.txt
+++ b/src/static/stats.txt
@@ -1,21 +1,21 @@
214 source files
-115904 lines
-4.3M size
+116142 lines
+ size
144 JavaScript source files
-106712 lines
-3.8M size
+106912 lines
+4.9M size
83 third party JavaScript source files
86259 lines
-3.0M size
+3.7M size
61 first party JavaScript source files
-20453 lines
-764K size
+20653 lines
+1.3M size
-3.5M uncompressed JavaScript size
-1.9M compressed JavaScript size
+ uncompressed JavaScript size
+ compressed JavaScript size
15 categories
-176 operations
+177 operations