aapssfc/book/common/ext/js-parsons/parsons.js
2015-03-03 15:56:44 +01:00

781 lines
28 KiB
JavaScript

(function($, _) { // wrap in anonymous function to not show some helper variables
// regexp used for trimming
var trimRegexp = /^\s*(.*?)\s*$/;
var formatVariableValue = function(varValue) {
var varType = typeof varValue;
if (varType === "undefined" || varValue === null) {
return "None";
} else if (varType === "string") { // show strings in quotes
return '"' + varValue + '"';
} else if (varType === "boolean") { // Python booleans with capital first letter
return varValue?"True":"False";
} else if ($.isArray(varValue)) { // JavaScript arrays
return '[' + varValue.join(', ') + ']';
} else if (varType === "object" && varValue.tp$name === "str") { // Python strings
return '"' + varValue.v + '"';
} else if (varType === "object" && varValue.tp$name === "list") { // Python lists
return '[' + varValue.v.join(', ') + ']';
} else {
return varValue;
}
};
var translations = {
fi: {
order: function() {
return "Ohjelma sisältää vääriä palasia tai palasten järjestys on väärä. Tämä on mahdollista korjata siirtämällä, poistamalla tai vaihtamalla korostettuja palasia.";},
lines_missing: function() {
return "Ohjelmassasi on liian vähän palasia, jotta se toimisi oikein.";},
no_matching: function(lineNro) {
return "Korostettu palanen (" + lineNro + ") on sisennetty Pythonin kieliopin vastaisesti."; },
block_structure: function(lineNro) {
return "Korostettu palanen (" + lineNro + ") on sisennetty väärään koodilohkoon."; },
unittest_error: function(errormsg) {
return "Virhe ohjelman jäsentämisessä/suorituksessa: <span class='errormsg'>" + errormsg + "</span>";
},
unittest_output_assertion: function(expected, actual) {
return "Odotettu tulostus: <span class='expected output'>" + expected + "</span>" +
"Ohjelmasi tulostus: <span class='actual output'>" + actual + "</span>";
},
unittest_assertion: function(expected, actual) {
return "Odotettu arvo: <span class='expected'>" + expected + "</span><br>" +
"Ohjelmasi antama arvo: <span class='actual'>" + actual + "</span>";
}
},
en: {
order: function() {
return "Code fragments in your program are wrong, or in wrong order. This can be fixed by moving, removing, or replacing highlighted fragments.";},
lines_missing: function() {
return "Your program has too few code fragments.";},
no_matching: function(lineNro) {
return "Based on python syntax, the highlighted fragment (" + lineNro + ") is not correctly indented."; },
block_structure: function(lineNro) { return "The highlighted fragment " + lineNro + " belongs to a wrong block (i.e. indentation)."; },
unittest_error: function(errormsg) {
return "Error in parsing/executing your program: <span class='errormsg'>" + errormsg + "</span>";
},
unittest_output_assertion: function(expected, actual) {
return "Expected output: <span class='expected output'>" + expected + "</span>" +
"Output of your program: <span class='actual output'>" + actual + "</span>";
},
unittest_assertion: function(expected, actual) {
return "Expected value: <span class='expected'>" + expected + "</span><br>" +
"Actual value: <span class='actual'>" + actual + "</span>";
}
},
enold: {
order: function() {
return "Some lines in incorrect position relative to the others.";},
lines_missing: function() {
return "Too few lines in your solution.";},
no_matching: function(lineNro) {
return "Line " + lineNro + " is not correctly indented. No matching indentation."; },
block_structure: function(lineNro) { return "Line " + lineNro + " is not indented correctly."; },
unittest_error: function(errormsg) {
return "Error in parsing/executing your program: <span class='errormsg'>" + errormsg + "</span>";
},
unittest_output_assertion: function(expected, actual) {
return "Expected output: <span class='expected output'>" + expected + "</span>" +
"Output of your program: <span class='actual output'>" + actual + "</span>";
},
unittest_assertion: function(expected, actual) {
return "Expected value: <span class='expected'>" + expected + "</span><br>" +
"Actual value: <span class='actual'>" + actual + "</span>";
}
}
};
var python_exec = function(code, variables) {
var output = "",
mainmod,
result = {'variables': {}},
varname;
Sk.configure( { output: function(str) { output += str; } } );
try {
mainmod = Sk.importMainWithBody("<stdin>", false, code);
} catch (e) {
return {"_output": output, "_error": "" + e};
}
for (var i = 0; i < variables.length; i++) {
varname = variables[i];
result.variables[varname] = mainmod.tp$getattr(varname);
}
result._output = output;
return result;
};
var python_indents = [],
spaces = "";
for (var counter = 0; counter < 20; counter++) {
python_indents[counter] = spaces;
spaces += " ";
}
var defaultToggleTypeHandlers = {
boolean: ["True", "False"],
compop: ["<", ">", "<=", ">=", "==", "!="],
mathop: ["+", "-", "*", "/"],
boolop: ["and", "or"],
range: function($item) {
var min = parseFloat($item.data("min") || "0", 10),
max = parseFloat($item.data("max") || "10", 10),
step = parseFloat($item.data("step") || "1", 10),
opts = [],
curr = min;
while (curr <= max) {
opts.push("" + curr);
curr += step;
}
return opts;
}
};
var addToggleableElements = function(widget) {
// toggleable elements are only enabled for unit tests
if (!widget.options.unittests) { return; }
var handlers = $.extend(defaultToggleTypeHandlers, widget.options.toggleTypeHandlers),
context = $("#" + widget.options.sortableId + ", #" + widget.options.trashId);
$(".jsparson-toggle", context).each(function(index, item) {
var type = $(item).data("type");
if (!type) { return; }
var handler = handlers[type],
jspOptions;
if ($.isFunction(handler)) {
jspOptions = handler($(item));
} else {
jspOptions = handler;
}
if (jspOptions && $.isArray(jspOptions)) {
$(item).attr("data-jsp-options", JSON.stringify(jspOptions));
}
});
context.on("click", ".jsparson-toggle", function() {
var $this = $(this),
curVal = $this.text(),
choices = $this.data("jsp-options");
$this.text(choices[(choices.indexOf(curVal) + 1)%choices.length]);
});
};
var ParsonsWidget = function(options) {
this.modified_lines = [];
this.extra_lines = [];
this.model_solution = [];
//To collect statistics, feedback should not be based on this
this.user_actions = [];
//State history for feedback purposes
this.state_path = [];
this.states = {};
var defaults = {
'incorrectSound': false,
'x_indent': 50,
'feedback_cb': false,
'first_error_only': true,
'max_wrong_lines': 10,
'trash_label': 'Drag from here',
'solution_label': 'Construct your solution here',
'lang': 'en',
'unittestId': 'unittest'
};
this.options = jQuery.extend({}, defaults, options);
this.feedback_exists = false;
this.id_prefix = options['sortableId'] + 'codeline';
if (translations.hasOwnProperty(this.options.lang)) {
this.translations = translations[this.options.lang];
} else {
this.translations = translations['en'];
}
this.FEEDBACK_STYLES = { 'correctPosition' : 'correctPosition',
'incorrectPosition' : 'incorrectPosition',
'correctIndent' : 'correctIndent',
'incorrectIndent' : 'incorrectIndent'};
};
//Public methods
ParsonsWidget.prototype.parseLine = function(spacePrefixedLine) {
return {
code: spacePrefixedLine.replace(trimRegexp, "$1").replace(/\\n/g,"\n"),
indent: spacePrefixedLine.length - spacePrefixedLine.replace(/^\s+/,"").length
};
};
ParsonsWidget.prototype.parseCode = function(lines, max_distractors) {
var distractors = [],
indented = [],
widgetData = [],
lineObject,
errors = [],
that = this;
$.each(lines, function(index, item) {
if (item.search(/#distractor\s*$/) >= 0) {
lineObject = {
code: item.replace(/#distractor\s*$/,"").replace(trimRegexp, "$1").replace(/\\n/,"\n"),
indent: -1,
distractor: true,
orig: index
};
if (lineObject.code.length > 0) {
distractors.push(lineObject);
}
} else {
lineObject = that.parseLine(item);
if (lineObject.code.length > 0) {
lineObject.distractor = false;
lineObject.orig = index;
indented.push(lineObject);
}
}
});
// Normalize indents and make sure indentation is valid
var normalized = this.normalizeIndents(indented);
$.each(normalized, function(index, item) {
if (item.indent < 0) {
errors.push(this.translations.no_matching(normalized.orig));
}
widgetData.push(item);
});
// Remove extra distractors
var permutation = this.getRandomPermutation(distractors.length);
var selected_distractors = [];
for (var i = 0; i < max_distractors; i++) {
selected_distractors.push(distractors[permutation[i]]);
widgetData.push(distractors[permutation[i]]);
}
return {
solution: $.extend(true, [], normalized),
distractors: $.extend(true, [], selected_distractors),
widgetInitial: $.extend(true, [], widgetData),
errors: errors};
};
ParsonsWidget.prototype.init = function(text) {
var initial_structures = this.parseCode(text.split("\n"), this.options.max_wrong_lines);
this.model_solution = initial_structures.solution;
this.extra_lines = initial_structures.distractors;
this.modified_lines = initial_structures.widgetInitial;
this.alternatives = {};
var that = this;
$.each(this.modified_lines, function(index, item) {
item.id = that.id_prefix + index;
item.indent = 0;
if (that.alternatives.hasOwnProperty(item.code)) {
that.alternatives[item.code].push(index);
} else {
that.alternatives[item.code] = [index];
}
});
};
ParsonsWidget.prototype.getHash = function(searchString) {
var ids = [];
var hash = [];
ids = $(searchString).sortable('toArray');
for (var i = 0; i < ids.length; i++) {
hash.push(ids[i].replace(this.id_prefix, "") + "_" + this.getLineById(ids[i]).indent);
}
//prefix with something to handle empty output situations
if (hash.length === 0) {
return "-";
} else {
return hash.join("-");
}
};
ParsonsWidget.prototype.solutionHash = function() {
return this.getHash("#ul-" + this.options.sortableId);
};
ParsonsWidget.prototype.trashHash = function() {
return this.getHash("#ul-" + this.options.trashId);
};
ParsonsWidget.prototype.whatWeDidPreviously = function() {
var hash = this.solutionHash();
var previously = this.states[hash];
if (!previously) { return undefined; }
var visits = _.filter(this.state_path, function(state) {
return state == hash;
}).length - 1;
var i, stepsToLast = 0, s,
outputStepTypes = ['removeOutput', 'addOutput', 'moveOutput'];
for (i = this.state_path.length - 2; i > 0; i--) {
s = this.states[this.state_path[i]];
if (s && outputStepTypes.indexOf(s.type) != -1) {
stepsToLast++;
}
if (hash === this.state_path[i]) { break; }
}
return $.extend(false, {'visits': visits, stepsToLast: stepsToLast}, previously);
};
ParsonsWidget.prototype.addLogEntry = function(entry) {
var state, previousState;
var logData = {
time: new Date(),
output: this.solutionHash(),
type: "action"
};
if (this.options.trashId) {
logData.input = this.trashHash();
}
if (entry.target) {
entry.target = entry.target.replace(this.id_prefix, "");
}
state = logData.output;
jQuery.extend(logData, entry);
this.user_actions.push(logData);
//Updating the state history
if(this.state_path.length > 0) {
previousState = this.state_path[this.state_path.length - 1];
this.states[previousState] = logData;
}
//Add new item to the state path only if new and previous states are not equal
if (this.state_path[this.state_path.length - 1] !== state) {
this.state_path.push(state);
// callback for reacting to actions
if ($.isFunction(this.options.action_cb)) {
this.options.action_cb.call(this, logData);
}
}
};
/**
* Update indentation of a line based on new coordinates
* leftDiff horizontal difference from (before and after drag) in px
***/
ParsonsWidget.prototype.updateIndent = function(leftDiff, id) {
var code_line = this.getLineById(id);
var new_indent = code_line.indent + Math.floor(leftDiff / this.options.x_indent);
new_indent = Math.max(0, new_indent);
code_line.indent = new_indent;
return new_indent;
};
/**
*
* @param id
* @return
*/
ParsonsWidget.prototype.getLineById = function(id) {
var index = -1;
for (var i = 0; i < this.modified_lines.length; i++) {
if (this.modified_lines[i].id == id) {
index = i;
break;
}
}
return this.modified_lines[index];
};
/** Does not use the current object - only the argument */
ParsonsWidget.prototype.normalizeIndents = function(lines) {
var normalized = [];
var new_line;
var match_indent = function(index) {
//return line index from the previous lines with matching indentation
for (var i = index-1; i >= 0; i--) {
if (lines[i].indent == lines[index].indent) {
return normalized[i].indent;
}
}
return -1;
};
for ( var i = 0; i < lines.length; i++ ) {
//create shallow copy from the line object
new_line = jQuery.extend({}, lines[i]);
if (i === 0) {
new_line.indent = 0;
if (lines[i].indent !== 0) {
new_line.indent = -1;
}
} else if (lines[i].indent == lines[i-1].indent) {
new_line.indent = normalized[i-1].indent;
} else if (lines[i].indent > lines[i-1].indent) {
new_line.indent = normalized[i-1].indent + 1;
} else {
// indentation can be -1 if no matching indentation exists, i.e. IndentationError in Python
new_line.indent = match_indent(i);
}
normalized[i] = new_line;
}
return normalized;
};
/**
* Retrieve the code lines based on what is in the DOM
*
* TODO(petri) refactor to UI
* */
ParsonsWidget.prototype.getModifiedCode = function(search_string) {
//ids of the the modified code
var lines_to_return = [],
that = this;
$(search_string).find("li").each(function(index, item) {
lines_to_return.push({id: $(item).attr("id"),
indent: parseInt($(item).css("margin-left"), 10)/that.options.x_indent});
});
return lines_to_return;
};
ParsonsWidget.prototype.hashToIDList = function(hash) {
var lines = [];
var lineValues;
var lineObject;
var h;
if (hash === "-" || hash === "" || hash === null) {
h = [];
} else {
h = hash.split("-");
}
var ids = [];
for (var i = 0; i < h.length; i++) {
lineValues = h[i].split("_");
ids.push(this.modified_lines[lineValues[0]].id);
}
return ids;
};
ParsonsWidget.prototype.updateIndentsFromHash = function(hash) {
var lines = [];
var lineValues;
var lineObject;
var h;
if (hash === "-" || hash === "" || hash === null) {
h = [];
} else {
h = hash.split("-");
}
var ids = [];
for (var i = 0; i < h.length; i++) {
lineValues = h[i].split("_");
this.modified_lines[lineValues[0]].indent = Number(lineValues[1]);
this.updateHTMLIndent(this.modified_lines[lineValues[0]].id);
}
return ids;
};
/**
* TODO(petri) refoctor to UI
*/
ParsonsWidget.prototype.displayError = function(message) {
if (this.options.incorrectSound && $.sound) {
$.sound.play(this.options.incorrectSound);
}
alert(message);
};
ParsonsWidget.prototype.colorFeedback = function(elemId) {
var student_code = this.normalizeIndents(this.getModifiedCode("#ul-" + elemId));
var lines_to_check = Math.min(student_code.length, this.model_solution.length);
var errors = [], log_errors = [];
var incorrectLines = [], lines = [];
var id, line, i;
var wrong_order = false;
//remove distractors from lines and add all those to the set of misplaced lines
for (i=0; i<student_code.length; i++) {
id = parseInt(student_code[i].id.replace(this.id_prefix, ""), 10);
line = this.getLineById(this.id_prefix + id);
if (line.distractor) {
incorrectLines.push(id);
wrong_order = true;
$("#" + this.id_prefix + id).addClass("incorrectPosition");
} else {
lines.push(id);
}
}
var inv = LIS.best_lise_inverse(lines);
var that = this;
_.each(inv, function(itemId) {
$("#" + that.id_prefix + itemId).addClass("incorrectPosition");
incorrectLines.push(itemId);
});
if (inv.length > 0 || errors.length > 0) {
wrong_order = true;
log_errors.push({type: "incorrectPosition", lines: incorrectLines});
}
if (wrong_order) {
errors.push(this.translations.order());
}
// Always show this feedback
if (this.model_solution.length < student_code.length) {
//$("#ul-" + elemId).addClass("incorrect");
//errors.push("Too many lines in your solution.");
log_errors.push({type: "tooManyLines", lines: student_code.length});
} else if (this.model_solution.length > student_code.length){
$("#ul-" + elemId).addClass("incorrect");
errors.push(this.translations.lines_missing());
log_errors.push({type: "tooFewLines", lines: student_code.length});
}
if (errors.length === 0) { // check indent if no other errors
for (i = 0; i < lines_to_check; i++) {
var code_line = student_code[i];
var model_line = this.model_solution[i];
if (code_line.indent !== model_line.indent &&
((!this.options.first_error_only) || errors.length === 0)) {
$("#" + code_line.id).addClass("incorrectIndent");
errors.push(this.translations.block_structure(i+1));
log_errors.push({type: "incorrectIndent", line: (i+1)});
}
if (code_line.code == model_line.code &&
code_line.indent == model_line.indent &&
errors.length === 0) {
$("#" + code_line.id).addClass("correctPosition");
}
}
}
if (errors.length === 0) {
$("#ul-" + elemId).addClass("correct");
}
return {errors: errors, log_errors: log_errors};
};
ParsonsWidget.prototype.unittest = function(unittests) {
var that = this,
feedback = "",
log_errors = [],
all_passed = true;
$.each(unittests, function(index, testdata) {
var $lines = $("#sortable li");
var student_code = that.normalizeIndents(that.getModifiedCode("#ul-sortable"));
var executableCode = "";
$.each(student_code, function(index, item) {
// split codeblocks on br elements
var lines = $("#" + item.id).html().split(/<br\s*\/?>/);
// go through all the lines
for (var i = 0; i < lines.length; i++) {
// add indents and get the text for the line (to remove the syntax highlight html elements)
executableCode += python_indents[item.indent] + $("<span>" + lines[i] + "</span>").text() + "\n";
}
});
executableCode += testdata.code;
var res = python_exec(executableCode, [testdata.variable]);
var testcaseFeedback = "",
success = true,
log_entry = {'code': testdata.code, 'msg': testdata.message},
expected_value,
actual_value;
if ("_error" in res) {
testcaseFeedback += that.translations.unittest_error(res._error);
success = false;
log_entry.type = "error";
log_entry.errormsg = res._error;
} else {
if (testdata.variable === "_output") { // checking output of the program
expected_value = testdata.expected;
actual_value = res._output;
testcaseFeedback += that.translations.unittest_output_assertion(expected_value, actual_value);
} else {
expected_value = formatVariableValue(testdata.expected);
actual_value = formatVariableValue(res.variables[testdata.variable]);
testcaseFeedback += that.translations.unittest_assertion(expected_value, actual_value);
}
log_entry.type = "assertion";
log_entry.variable = testdata.variable;
log_entry.expected = expected_value;
log_entry.actual = actual_value;
if (actual_value != expected_value) { // should we do a strict test??
success = false;
}
}
all_passed = all_passed && success;
log_entry.success = success;
log_errors.push(log_entry);
feedback += "<div class='testcase " + (success?"correct":"incorrect") +
"'><span class='msg'>" + testdata.message + "</span><br>" +
testcaseFeedback + "</div>";
});
if (all_passed) {
$("#ul-" + this.options.sortableId).addClass("correct");
}
return { errors: feedback, "log_errors": log_errors, success: all_passed };
};
/**
* @return
* TODO(petri): Separate UI from here
*/
ParsonsWidget.prototype.getFeedback = function() {
var fb;
this.feedback_exists = true;
if (typeof(this.options.unittests) !== "undefined") { /// unittests are specified
fb = this.unittest(this.options.unittests);
this.addLogEntry({type: "feedback", errors: fb.log_errors});
return { feedback: fb.errors, success: fb.success };
} else { // "traditional" parson feedback
fb = this.colorFeedback(this.options.sortableId);
if (this.options.feedback_cb) {
this.options.feedback_cb(fb); //TODO(petri): what is needed?
}
this.addLogEntry({type: "feedback", errors: fb.log_errors});
return fb.errors;
}
};
ParsonsWidget.prototype.clearFeedback = function() {
if (this.feedback_exists) {
$("#ul-" + this.options.sortableId).removeClass("incorrect correct");
var li_elements = $("#ul-" + this.options.sortableId + " li");
$.each(this.FEEDBACK_STYLES, function(index, value) {
li_elements.removeClass(value);
});
}
this.feedback_exists = false;
};
ParsonsWidget.prototype.getRandomPermutation = function(n) {
var permutation = [];
var i;
for (i = 0; i < n; i++) {
permutation.push(i);
}
var swap1, swap2, tmp;
for (i = 0; i < n; i++) {
swap1 = Math.floor(Math.random() * n);
swap2 = Math.floor(Math.random() * n);
tmp = permutation[swap1];
permutation[swap1] = permutation[swap2];
permutation[swap2] = tmp;
}
return permutation;
};
ParsonsWidget.prototype.shuffleLines = function() {
var permutation = this.getRandomPermutation(this.modified_lines.length);
var idlist = [];
for(var i in permutation) {
idlist.push(this.modified_lines[permutation[i]].id);
}
if (this.options.trashId) {
this.createHTMLFromLists([],idlist);
} else {
this.createHTMLFromLists(idlist,[]);
}
addToggleableElements(this);
};
ParsonsWidget.prototype.createHTMLFromHashes = function(solutionHash, trashHash) {
var solution = this.hashToIDList(solutionHash);
var trash = this.hashToIDList(trashHash);
this.createHTMLFromLists(solution,trash);
this.updateIndentsFromHash(solutionHash);
};
ParsonsWidget.prototype.updateHTMLIndent = function(codelineID) {
var line = this.getLineById(codelineID);
$('#' + codelineID).css("margin-left", this.options.x_indent * line.indent + "px");
};
ParsonsWidget.prototype.codeLineToHTML = function(codeline) {
return '<li id="' + codeline.id + '" class="prettyprint lang-py">' + codeline.code + '<\/li>';
};
ParsonsWidget.prototype.codeLinesToHTML = function(codelineIDs, destinationID) {
var lineHTML = [];
for(var id in codelineIDs) {
var line = this.getLineById(codelineIDs[id]);
lineHTML.push(this.codeLineToHTML(line));
}
return '<ul id="ul-' + destinationID + '">'+lineHTML.join('')+'</ul>';
};
/** modifies the DOM by inserting exercise elements into it */
ParsonsWidget.prototype.createHTMLFromLists = function(solutionIDs, trashIDs) {
var html;
if (this.options.trashId) {
html = (this.options.trash_label?'<p>'+this.options.trash_label+'</p>':'') +
this.codeLinesToHTML(trashIDs, this.options.trashId);
$("#" + this.options.trashId).html(html);
html = (this.options.solution_label?'<p>'+this.options.solution_label+'</p>':'') +
this.codeLinesToHTML(solutionIDs, this.options.sortableId);
$("#" + this.options.sortableId).html(html);
} else {
html = this.codeLinesToHTML(solutionIDs, this.options.sortableId);
$("#" + this.options.sortableId).html(html);
}
if (window.prettyPrint && (typeof(this.options.prettyPrint) === "undefined" || this.options.prettyPrint)) {
prettyPrint();
}
var that = this;
var sortable = $("#ul-" + this.options.sortableId).sortable(
{
start : function() { that.clearFeedback(); },
stop : function(event, ui) {
if ($(event.target)[0] != ui.item.parent()[0]) {
return;
}
that.updateIndent(ui.position.left - ui.item.parent().offset().left,
ui.item[0].id);
that.updateHTMLIndent(ui.item[0].id);
that.addLogEntry({type: "moveOutput", target: ui.item[0].id}, true);
},
receive : function(event, ui) {
var ind = that.updateIndent(ui.position.left - ui.item.parent().offset().left,
ui.item[0].id);
that.updateHTMLIndent(ui.item[0].id);
that.addLogEntry({type: "addOutput", target: ui.item[0].id}, true);
},
grid : [that.options.x_indent, 1 ]
});
sortable.addClass("output");
if (this.options.trashId) {
var trash = $("#ul-" + this.options.trashId).sortable(
{
connectWith: sortable,
start: function() { that.clearFeedback(); },
receive: function(event, ui) {
that.getLineById(ui.item[0].id).indent = 0;
that.updateHTMLIndent(ui.item[0].id);
that.addLogEntry({type: "removeOutput", target: ui.item[0].id}, true);
},
stop: function(event, ui) {
if ($(event.target)[0] != ui.item.parent()[0]) {
// line moved to output and logged there
return;
}
that.addLogEntry({type: "moveInput", target: ui.item[0].id}, true);
}
});
sortable.sortable('option', 'connectWith', trash);
}
this.addLogEntry({type: 'init', time: new Date(), bindings: this.modified_lines});
};
window['ParsonsWidget'] = ParsonsWidget;
}
// allows _ and $ to be modified with noconflict without changing the globals
// that parsons uses
)($,_);