openct-tasks/_common/modules/pemFioi/quickAlgo/python_interface.js

1152 lines
39 KiB
JavaScript

/*
python_interface:
Python mode interface and running logic.
*/
function LogicController(maxInstructions, subTask) {
/**
* Class properties
*/
this.subTask = subTask;
this._maxInstructions = maxInstructions || null;
this.language = 'python';
this._textFile = null;
this._extended = false;
// for quickpi additional will contain the string containing all sensors in xml
this.programs = [{
blockly: null,
additional: null,
blocklyJS: null,
javascript: null
}];
this._aceEditor = null;
this._workspace = null;
this._prevWidth = 0;
this._startingBlock = true;
this._visible = true;
this._strings = window.languageStrings;
this._options = {};
this._readOnly = false;
this.includeBlocks = null;
/** @type {React.Component|null} */
this.analysisComponent = null;
this.loadContext = function (mainContext) {
this._mainContext = mainContext;
};
this.savePrograms = function (full) {
if(this._aceEditor) {
this.programs[0].blockly = this._aceEditor.getValue();
if (full) {
var additional = {};
if (window.quickAlgoInterface && window.quickAlgoInterface.saveAdditional)
window.quickAlgoInterface.saveAdditional(additional);
this.programs[0].additional = additional;
}
}
};
this.loadPrograms = function () {
if(this._aceEditor && this.programs[0].blockly) {
this._aceEditor.setValue(''+this.programs[0].blockly);
this._aceEditor.selection.clearSelection();
}
if (this._aceEditor && this.programs[0].additional) {
if (window.quickAlgoInterface && window.quickAlgoInterface.loadAdditional)
window.quickAlgoInterface.loadAdditional(this.programs[0].additional);
}
};
this.loadExample = function (example) {
if(!example.python) { return; }
this._aceEditor.setValue('' + example.python + '\n\n' + this._aceEditor.getValue());
var Range = ace.require('ace/range').Range;
this._aceEditor.selection.setRange(new Range(0, 0, example.python.split(/\r\n|\r|\n/).length, 0));
};
this.switchLanguage = function (e) {
this.language = e.value;
};
this.load = function (language, display, nbTestCases, options) {
if (this.skulptAnalysisEnabled() && !this.skulptAnalysisShouldByEnabled()) {
console.log('Module "python-analysis" is loaded but not used.');
}
this._options = options;
this._loadBasicEditor();
if(this._aceEditor && ! this._aceEditor.getValue()) {
if(options.defaultCode !== undefined)
this._aceEditor.setValue(options.defaultCode);
else
this._aceEditor.setValue(this.getDefaultContent());
}
};
this.unload = function () {
this.stop();
this._unbindEditorEvents();
};
this.unloadLevel = this.unload;
this.getCode = function(language) {
if (language == "python")
return this._aceEditor.getValue();
return "";
};
this.checkCode = function(code, display) {
// Check a code before validation; display is a function which will get
// error messages
var forbidden = pythonForbidden(code, this.includeBlocks);
if(!display) {
display = function() {};
}
if(forbidden) {
display("Le mot-clé "+forbidden+" est interdit ici !");
return false;
}
if(maxInstructions && pythonCount(code) > maxInstructions) {
display("Vous utilisez trop d'éléments Python !");
return false;
}
var limited = this.findLimited(code);
if(limited && limited.type == 'uses') {
display('Vous utilisez trop souvent un mot-clé à utilisation limitée : "'+limited.name+'".');
return false;
} else if(limited && limited.type == 'assign') {
display('Vous n\'avez pas le droit de réassigner un mot-clé à utilisation limitée : "'+limited.name+'".');
return false;
}
if(pythonCount(code) <= 0) {
display("Vous ne pouvez pas valider un programme vide !");
return false;
}
var availableModules = this.getAvailableModules();
for(var i=0; i < availableModules.length; i++) {
var match = new RegExp('from\\s+' + availableModules[i] + '\\s+import\\s+\\*');
match = match.exec(code);
if(match === null) {
display("Vous devez mettre la ligne <code>from " + availableModules[i] + " import *</code> dans votre programme.");
return false;
}
}
// Check for functions used as values
var re = /def\W+([^(]+)\(/g;
var foundFuncs = this._mainContext && this._mainContext.runner ? this._mainContext.runner.getDefinedFunctions() : [];
var match;
while(match = re.exec(code)) {
foundFuncs.push(match[1]);
}
for(var j=0; j<foundFuncs.length; j++) {
var re = new RegExp('\\W' + foundFuncs[j] + '([^A-Za-z0-9_( ]| +[^ (]|$)');
if(re.exec(code)) {
display("Vous utilisez la fonction '" + foundFuncs[j] + "' sans les parenthèses. Ajoutez les parenthèses pour appeler la fonction.");
return false;
}
}
return true;
};
this.checkCodes = function(codes, display) {
// Check multiple codes before validation
for(var i = 0; i < codes.length; i++) {
if(!this.checkCode(codes[i], display)) {
return false;
}
}
return true;
};
this.getDefaultContent = function () {
if(this._options.startingExample && this._options.startingExample.python) {
return this._options.startingExample.python;
}
var availableModules = this.getAvailableModules();
var content = '';
for(var i=0; i < availableModules.length; i++) {
content += 'from ' + availableModules[i] + ' import *\n';
}
return content;
};
/**
* Code running specific operations
*/
this.stopAndTryAgain = function () {
this.stop();
if(type == 'run') {
window.setTimeout(this.run.bind(this), 100);
} else if(type == 'step') {
window.setTimeout(this.step.bind(this), 100);
}
};
this.getLanguage = function () {
return this.language;
};
this.getAllCodes = function(answer) {
// Generate codes for each node
var codes = [];
for (var iNode = 0; iNode < this._mainContext.nbNodes; iNode++) {
if(this._mainContext.codeIdForNode) {
var iCode = this._mainContext.codeIdForNode(iNode);
} else {
var iCode = Math.min(iNode, this._mainContext.nbCodes-1);
}
if(answer) {
// Generate codes for specified answer
codes[iNode] = answer[iCode].blockly;
} else {
// Generate codes for current program
codes[iNode] = this.programs[iCode].blockly;
}
}
return codes;
},
this.prepareRun = function (type) {
if (!this._mainContext) { return false; }
var nbRunning = this._mainContext.runner.nbRunning();
if (nbRunning > 0) {
this.stopAndTryAgain(type);
return false;
}
// Get code
this.savePrograms();
var codes = this.getAllCodes();
var code = codes[0];
// TODO :: check all codes
// Abort if code is not valid
if(!this.checkCode(code, function(err) {
if(window.quickAlgoInterface) {
window.quickAlgoInterface.displayError(err);
window.quickAlgoInterface.setPlayPause(false);
} else {
$('#errors').html(err);
}
})) {
return false;
}
// Initialize runner
this._mainContext.runner.initCodes(codes);
if (this.skulptAnalysisEnabled()) {
this.loadSkulptAnalysis();
}
return true;
};
this.run = function () {
if(!this.prepareRun('run')) {
return;
}
this._mainContext.runner.run();
};
this.step = function () {
var self = this;
if(!this._mainContext.runner._isRunning) {
// No run in progress, start a new one
if(!this.prepareRun('step')) {
return;
}
}
this._mainContext.runner.runStep(function() {
// After the step is complete.
if (self.skulptAnalysisEnabled() && self.analysisComponent) {
// Compute and update the internal analysis.
var skulptSuspensions = self._mainContext.runner._debugger.suspension_stack;
var oldAnalysis = self.analysisComponent.props.analysis;
self.analysisComponent.props.analysis = analyseSkulptState(skulptSuspensions, oldAnalysis);
self.analysisComponent.forceUpdate();
}
});
};
this.stop = function () {
if(this._mainContext.runner) {
this._mainContext.runner.stop();
}
};
/**
* IO specific operations
*/
this.handleFiles = function (files) {
var that = this;
if (files.length < 0) {
return;
}
var file = files[0];
var textType = /text.*/;
if (file.type.match(textType)) {
var reader = new FileReader();
reader.onload = function (e) {
var code = reader.result;
if (code[0] == "<") {
// XXX :: what is this code about? Is it actually used? Blockly isn't
// even loaded
try {
var xml = Blockly.Xml.textToDom(code);
that.programs[0][that.player].blockly = code;
} catch (e) {
if(window.quickAlgoInterface) {
window.quickAlgoInterface.displayError(that._strings.invalidContent);
} else {
$("#errors").html(that._strings.invalidContent);
}
}
} else {
// The 5 come from this string: '# {"' It must be higher in order to not fail
if (that._mainContext.loadAdditional && code[0] === '#' && code.length > 5) {
// This var correspond on how it is saved with JSON.stringify, these are the first characters
// in order to be allowed to load codes which are from this version (our current corrections) it is
// better to test if the first characters corresponds to our valid json instead of being regular comments.
// This can fail only in the case when you start your comment with: '# {"'
var firstChars = "{\"";
var toVerify = code.substring(2, 2 + firstChars.length);
if (toVerify === firstChars) {
var additionalStr = code.substring(2, code.indexOf('\n'));
var newCode = code.substring(code.indexOf('\n') + 1);
that.programs[0].additional = JSON.parse(additionalStr);
that.programs[0].blockly = newCode;
} else {
that.programs[0].blockly = code;
that.programs[0].additional = {};
}
} else {
that.programs[0].blockly = code;
that.programs[0].additional = {};
}
}
that.loadPrograms();
};
reader.readAsText(file);
} else {
if(window.quickAlgoInterface) {
window.quickAlgoInterface.displayError(this._strings.unknownFileType);
} else {
$("#errors").html(this._strings.unknownFileType);
}
}
};
this.getCodeWithAdditional = function() {
return "# " + JSON.stringify(this.programs[0].additional) + "\n" + this.programs[0].blockly;
};
this.saveProgram = function () {
this.savePrograms(true);
var code = this.getCodeWithAdditional();
var data = new Blob([code], { type: 'text/plain' });
// If we are replacing a previously generated file we need to
// manually revoke the object URL to avoid memory leaks.
if (this.textFile !== null) {
window.URL.revokeObjectURL(this.textFile);
}
this.textFile = window.URL.createObjectURL(data);
// returns a URL you can use as a href
$("#saveUrl").html("<a id='downloadAnchor' href='" + this.textFile + "' download='robot_python_program.txt'>" + this._strings.download + "</a>");
var downloadAnchor = document.getElementById('downloadAnchor');
downloadAnchor.click();
return this.textFile;
};
/**
* Getters & Setters
*/
this.setLocalization = function (localization) {
this._localization = localization;
};
this.getLocalization = function () {
return this._localization;
};
this.getLocalizedStrings = function () {
return this._strings;
};
this.setIncludeBlocks = function (blocks) {
this.includeBlocks = blocks;
this.updateTaskIntro();
};
this.setMainContext = function (mainContext) {
this._mainContext = mainContext;
};
this.isVisible = function () {
return this._visible;
};
/**
* DOM specific operations
*/
this._loadEditorWorkSpace = function () {
return "<div id='python-analysis'></div>" +
"<div id='blocklyContainer'>" + // TODO :: change ID here and in CSS
"<div id='python-workspace' class='language_python' style='width: 100%; height: 100%'></div>" +
"</div>";
};
this._loadBasicEditor = function () {
if (this._mainContext.display) {
$('#languageInterface').html(
this._loadEditorWorkSpace()
);
if(window.quickAlgoResponsive) {
$('#blocklyLibContent').prepend('<div class="pythonIntroSimple"></div>');
$('#editorBar').prependTo('#languageInterface');
}
this._loadAceEditor();
this._bindEditorEvents();
this.updateTaskIntro();
}
};
/**
* Load the skulp analysis block in React.
*/
this.loadSkulptAnalysis = function() {
var self = this;
var domContainer = document.querySelector('#python-analysis');
ReactDOM.render(React.createElement(PythonStackViewContainer, {
ref: function(componentReference) {
if (componentReference) {
self.analysisComponent = componentReference;
/**
* Move the analysis container with the mouse.
*/
document.addEventListener('mouseup', function () {
self.analysisComponent.mouseUpHandler();
});
document.addEventListener('mousemove', function (event) {
self.analysisComponent.mouseMoveHandler(event.clientX, event.clientY);
});
}
},
analysis: null,
show: true
}), domContainer);
};
/**
* Whether skulpt analysis should be enabled given the current task.
*
* @return {boolean}
*/
this.skulptAnalysisShouldByEnabled = function() {
var variablesEnabled = true;
var taskInfos = this._mainContext.infos;
var forbidden = pythonForbiddenLists(taskInfos.includeBlocks).forbidden;
if (forbidden.indexOf('var_assign') !== -1) {
variablesEnabled = false;
}
return variablesEnabled;
};
/**
* Whether skulpt analysis is enabled.
*
* @return {boolean}
*/
this.skulptAnalysisEnabled = function() {
return (this.language === 'python' && typeof analyseSkulptState === 'function');
};
/**
* Clears the skulpt analysis window.
*/
this.clearSkulptAnalysis = function() {
if (this.skulptAnalysisEnabled() && this.analysisComponent) {
this.analysisComponent.props.analysis = null;
this.analysisComponent.forceUpdate();
}
};
/**
* Shows the skulpt analysis window.
*/
this.showSkulptAnalysis = function() {
if (this.skulptAnalysisEnabled() && this.analysisComponent) {
this.analysisComponent.props.show = true;
this.analysisComponent.forceUpdate();
}
};
/**
* Hides the skulpt analysis window.
*/
this.hideSkulptAnalysis = function() {
if (this.skulptAnalysisEnabled() && this.analysisComponent) {
this.analysisComponent.props.show = false;
this.analysisComponent.forceUpdate();
}
};
this.onResize = function() {
// On resize function to be called by the interface
this._aceEditor.resize();
};
this._addAutoCompletion = function() {
function getSnippet(proto) {
var parenthesisOpenIndex = proto.indexOf("(");
if (proto.charAt(parenthesisOpenIndex + 1) == ')') {
return proto;
} else {
var ret = proto.substring(0, parenthesisOpenIndex + 1);
var commaIndex = parenthesisOpenIndex;
var snippetIndex = 1;
while (proto.indexOf(',', commaIndex + 1) != -1) {
var newCommaIndex = proto.indexOf(',', commaIndex + 1);
// we want to keep the space.
if (proto.charAt(commaIndex + 1) == ' ') {
commaIndex += 1;
ret += ' ';
}
ret += "${" + snippetIndex + ':';
ret += proto.substring(commaIndex + 1, newCommaIndex);
ret += "},";
commaIndex = newCommaIndex;
snippetIndex += 1;
}
// the last one is with the closing parenthesis.
var parenthesisCloseIndex = proto.indexOf(')');
if (proto.charAt(commaIndex + 1) == ' ') {
commaIndex += 1;
ret += ' ';
}
ret += "${" + snippetIndex + ':';
ret += proto.substring(commaIndex + 1, parenthesisCloseIndex);
ret += "})";
return ret;
}
}
var langTools = ace.require("ace/ext/language_tools");
// This array will contain all functions for which we must add autocompletion
var completions = [];
// we add completion on functions
if (this.includeBlocks && this.includeBlocks.generatedBlocks) {
for (var categoryIndex in this.includeBlocks.generatedBlocks) {
for (var funIndex in this.includeBlocks.generatedBlocks[categoryIndex]) {
var fun = this.includeBlocks.generatedBlocks[categoryIndex][funIndex];
var funInfos = this._getFunctionsInfo(fun);
var funProto = funInfos.proto;
var funHelp = funInfos.help;
var funSnippet = getSnippet(funProto);
completions.push({
caption: funProto,
snippet: funSnippet,
type: "snippet",
docHTML: "<b>" + funProto + "</b><hr></hr>" + funHelp
})
}
}
if(this._mainContext.customConstants && this._mainContext.customConstants[categoryIndex]) {
var constList = this._mainContext.customConstants[categoryIndex];
for(var iConst=0; iConst < constList.length; iConst++) {
var name = constList[iConst].name;
if(this._mainContext.strings.constant && this._mainContext.strings.constant[name]) {
name = this._mainContext.strings.constant[name];
}
completions.push({
name: name,
value: name,
meta: this._strings.constant
})
}
}
}
// Adding allowed consts (for, while...)
var allowedConsts = pythonForbiddenLists(this.includeBlocks).allowed;
hideHiddenWords(allowedConsts);
// This blocks are blocks which are not special but must be added
var toAdd = ["True", "False"];
for (var toAddId = 0; toAddId < toAdd.length; toAddId++) {
allowedConsts.push(toAdd[toAddId]);
}
var keywordi18n = this._strings.keyword;
// if we want to modify the result of certain keys
var specialSnippets = {
// list_brackets and dict_brackets are not working
list_brackets:
{
name: "[]",
value: "[]",
meta: keywordi18n
},
dict_brackets: {
name: "{}",
value: "{}",
meta: keywordi18n
},
var_assign: {
caption: "x =",
snippet: "x = $1",
type: "snippet",
meta: this._strings.variable
},
if: {
caption: "if",
snippet: "if ${1:condition}:\n\t${2:pass}",
type: "snippet",
meta: keywordi18n
},
while: {
caption: "while",
snippet: "while ${1:condition}:\n\t${2:pass}",
type: "snippet",
meta: keywordi18n
},
elif: {
caption: "elif",
snippet: "elif ${1:condition}:\n\t${2:pass}",
type: "snippet",
meta: keywordi18n
}
};
for (var constId = 0; constId < allowedConsts.length; constId++) {
if (specialSnippets.hasOwnProperty(allowedConsts[constId])) {
// special constant, need to create snippet
completions.push(specialSnippets[allowedConsts[constId]]);
} else {
// basic constant (just printed)
completions.push({
name: allowedConsts[constId],
value: allowedConsts[constId],
meta: keywordi18n
})
}
}
// creating the completer
var completer = {
getCompletions : function(editor, session, pos, prefix, callback) {
callback(null, completions);
}
};
// we set the completer to only what we want instead of all the noisy default stuff
if(langTools) { langTools.setCompleters([completer]); }
};
this._loadAceEditor = function () {
this._aceEditor = ace.edit('python-workspace');
if (!this._mainContext.disableAutoCompletion)
this._addAutoCompletion();
this._aceEditor.setOptions({
readOnly: !!this._options.readOnly,
enableBasicAutocompletion: !this._mainContext.disableAutoCompletion,
enableLiveAutocompletion: !this._mainContext.disableAutoCompletion,
enableSnippets: false
});
this._aceEditor.$blockScrolling = Infinity;
this._aceEditor.getSession().setMode("ace/mode/python");
this._aceEditor.setFontSize(16);
if (!this._mainContext.disableAutoCompletion) {
// we resize the completer window, because some functions are too big so we need more place:
if (!this._aceEditor.completer) {
// make sure completer is initialized
this._aceEditor.execCommand("startAutocomplete");
this._aceEditor.completer.detach();
}
this._aceEditor.completer.popup.container.style.width = "22%";
// removal of return for autocomplete
if (this._aceEditor.completer.keyboardHandler.commandKeyBinding.return)
delete this._aceEditor.completer.keyboardHandler.commandKeyBinding.return;
}
};
this.findLimited = function(code) {
if(this._mainContext.infos.limitedUses) {
return pythonFindLimited(code, this._mainContext.infos.limitedUses, this._mainContext.strings.code);
} else {
return false;
}
};
this.getCapacityInfo = function() {
// Handle capacity display
var code = this._aceEditor.getValue();
var forbidden = pythonForbidden(code, this.includeBlocks);
if(forbidden) {
return {text: "Mot-clé interdit utilisé : "+forbidden, invalid: true, type: 'forbidden'};
}
var text = '';
var remaining = 1;
if(maxInstructions) {
remaining = maxInstructions - pythonCount(code);
var optLimitElements = {
maxBlocks: maxInstructions,
remainingBlocks: Math.abs(remaining)
};
var strLimitElements = remaining < 0 ? this._strings.limitElementsOver : this._strings.limitElements;
text = strLimitElements.format(optLimitElements);
}
if(remaining < 0) {
return {text: text, invalid: true, type: 'capacity'};
}
var limited = this.findLimited(code);
if(limited && limited.type == 'uses') {
return {text: 'Vous utilisez trop souvent un mot-clé à utilisation limitée : "'+limited.name+'".', invalid: true, type: 'limited'};
} else if(limited && limited.type == 'assign') {
return {text: 'Vous n\'avez pas le droit de réassigner un mot-clé à utilisation limitée : "'+limited.name+'".', invalid: true, type: 'limited'};
} else if(remaining == 0) {
return {text: text, warning: true, type: 'capacity'};
}
return {text: text, type: 'capacity'};
};
this._removeDropDownDiv = function() {
$('.blocklyDropDownDiv').remove();
};
this._bindEditorEvents = function () {
$('body').on('click', this._removeDropDownDiv);
var that = this;
var onEditorChange = function () {
if(!that._aceEditor) { return; }
if(that._mainContext.runner && that._mainContext.runner._editorMarker) {
that.clearSkulptAnalysis();
that._aceEditor.session.removeMarker(that._mainContext.runner._editorMarker);
that._mainContext.runner._editorMarker = null;
}
// Interrupt any ongoing execution
if(that._mainContext.runner) {
that._mainContext.runner.reset();
}
if(window.quickAlgoInterface) {
window.quickAlgoInterface.displayError(null);
} else {
$("#errors").html('');
}
if(window.quickAlgoInterface) {
window.quickAlgoInterface.displayCapacity(that.getCapacityInfo());
} else {
$('#capacity').html(that.getCapacityInfo().text);
}
if(that.subTask) {
that.subTask.onChange();
}
// Close reportValue popups
$('.blocklyDropDownDiv').remove();
};
this._aceEditor.getSession().on('change', debounce(onEditorChange, 500, false))
};
this._unbindEditorEvents = function () {
$('body').off('click', this._removeDropDownDiv);
};
this.getAvailableModules = function () {
if(this.includeBlocks && this.includeBlocks.generatedBlocks) {
var availableModules = [];
for (var generatorName in this.includeBlocks.generatedBlocks) {
if(this.includeBlocks.generatedBlocks[generatorName].length) {
availableModules.push(generatorName);
}
}
return availableModules;
} else {
return [];
}
};
/**
* This method allow us to get the informations about the function, pasted from updateTaskIntro
* This function was separated from updateTaskIntro because it will also be used by the
* autocompletion generator.
* @param functionName The name of the function
* @return {{help: string, proto: string, desc: *}} The informations about the function
*/
this._getFunctionsInfo = function(functionName) {
var blockDesc = '', funcProto = '', blockHelp = '';
if (this._mainContext.docGenerator) {
blockDesc = this._mainContext.docGenerator.blockDescription(functionName);
funcProto = blockDesc.substring(blockDesc.indexOf('<code>') + 6, blockDesc.indexOf('</code>'));
blockHelp = blockDesc.substring(blockDesc.indexOf('</code>') + 7);
} else {
var blockName = functionName;
var funcCode = this._mainContext.strings.code[blockName] || blockName;
blockDesc = this._mainContext.strings.description[blockName];
if(blockDesc) {
blockDesc = blockDesc.replace(/@/g, funcCode);
}
if (!blockDesc) {
funcProto = funcCode + '()';
blockDesc = '<code>' + funcProto + '</code>';
} else if (blockDesc.indexOf('</code>') < 0) {
var funcProtoEnd = blockDesc.indexOf(')') + 1;
if(funcProtoEnd > 0) {
funcProto = blockDesc.substring(0, funcProtoEnd);
blockHelp = blockDesc.substring(funcProtoEnd);
blockDesc = '<code>' + funcProto + '</code>' + blockHelp;
} else {
console.error("Description for block '" + blockName + "' needs to be of the format 'function() : description', auto-generated one used instead could be wrong.");
funcProto = blockName + '()';
blockDesc = '<code>' + funcProto + '</code> : ' + blockHelp;
}
}
}
return {
desc: blockDesc,
proto: funcProto,
help: blockHelp
};
};
function hideHiddenWords(list) {
var hiddenWords = ['__getitem__', '__setitem__'];
for(var i = 0; i < hiddenWords.length; i++) {
var word = hiddenWords[i];
var wIdx = list.indexOf(word);
if(wIdx > -1) {
list.splice(wIdx, 1);
}
}
}
this.updateTaskIntro = function () {
if(!this._mainContext.display) { return; }
if($('.pythonIntro').length == 0) {
quickAlgoInterface.appendTaskIntro('<hr class="pythonIntroElement long" />'
+ '<div class="pythonIntro pythonIntroElement long">'
+ ' <div class="pythonIntroSimple"></div>'
+ ' <div class="pythonIntroFull"></div>'
+ ' <div class="pythonIntroBtn"></div>'
+ '</div>');
}
$('.pythonIntro').off('click', 'code');
if(this._mainContext.infos.noPythonHelp) {
$('.pythonIntroElement').css('display', 'none');
return;
}
$('.pythonIntroElement').css('display', '');
// Display a list for the simpleHtml version
function displaySimpleList(elemList) {
var html = '';
if(window.quickAlgoResponsive && elemList.length > 0) {
// Dropdown mode
html = '<div class="pythonIntroSelect">';
html += '<select>';
for(var i=0 ; i < elemList.length; i++) {
var elem = elemList[i];
html += '<option' + (elem.desc ? ' data-desc="' + elem.desc.replace('"', '&quot;') + '"' : '') + '>';
html += (typeof elem == 'string' ? elem : elem.func);
html += '</option>';
}
html += '</select>';
html += '<div class="pythonIntroSelectBtn pythonIntroSelectBtnCopy"><span class="fas fa-clone"></span></div>';
html += '<div class="pythonIntroSelectBtn pythonIntroSelectBtnHelp"><span class="fas fa-question"></span></div>';
html += '<span class="pythonIntroSelectDesc"></span>';
html += '</div>';
} else {
// Normal mode
for(var i=0 ; i < elemList.length; i++) {
var elem = elemList[i];
if(i > 0) { html += ', '; }
html += '<code>' + (typeof elem == 'string' ? elem : elem.func) + '</code>';
}
}
return html;
};
var fullHtml = '';
var simpleHtml = '';
var availableModules = this.getAvailableModules();
if(availableModules.length) {
fullHtml += '<p>' + (availableModules.length > 1) ?
window.languageStrings.startingLine :
window.languageStrings.startingLines;
fullHtml += ' :</p>'
+ '<p><code>'
+ 'from ' + availableModules[0] + ' import *';
for(var i=1; i < availableModules.length; i++) {
fullHtml += '\nfrom ' + availableModules[i] + ' import *';
}
fullHtml += '</code></p>'
+ '<p>' + window.languageStrings.availableFunctionsVerbose + '</p>'
+ '<ul>';
simpleHtml += window.languageStrings.availableFunctions;
var availableConsts = [];
// Generate list of functions available
var simpleElements = [];
for (var generatorName in this.includeBlocks.generatedBlocks) {
var blockList = this.includeBlocks.generatedBlocks[generatorName];
for (var iBlock=0; iBlock < blockList.length; iBlock++) {
var infos = this._getFunctionsInfo(blockList[iBlock]);
var blockDesc = infos.desc;
var funcProto = infos.proto;
var blockHelp = infos.help;
fullHtml += '<li>' + blockDesc + '</li>';
simpleElements.push({func: funcProto, desc: blockHelp});
}
// Handle constants as well
if(this._mainContext.customConstants && this._mainContext.customConstants[generatorName]) {
var constList = this._mainContext.customConstants[generatorName];
for(var iConst=0; iConst < constList.length; iConst++) {
var name = constList[iConst].name;
if(this._mainContext.strings.constant && this._mainContext.strings.constant[name]) {
name = this._mainContext.strings.constant[name];
}
availableConsts.push(name);
}
}
}
simpleHtml += displaySimpleList(simpleElements);
fullHtml += '</ul>';
if(availableConsts.length) {
fullHtml += '<p>Les constantes disponibles sont : <code>' + availableConsts.join('</code>, <code>') + '</code>.</p>';
simpleHtml += '<br />Constantes disponibles : ' + displaySimpleList(availableConsts);
}
}
var pflInfos = pythonForbiddenLists(this.includeBlocks);
function processForbiddenList(origList, allowed) {
var list = origList.slice();
hideHiddenWords(list);
var bracketsWords = { list_brackets: 'crochets [ ]+[]', dict_brackets: 'accolades { }+{}', var_assign: 'variables+x =' };
for(var bracketsCode in bracketsWords) {
var bracketsIdx = list.indexOf(bracketsCode);
if(bracketsIdx >= 0) {
list[bracketsIdx] = bracketsWords[bracketsCode];
}
}
var word = allowed ? window.languageStrings.keywordAllowed : window.languageStrings.keywordForbidden;
var words = allowed ? window.languageStrings.keywordsAllowed : window.languageStrings.keywordsForbidden;
var cls = allowed ? '' : ' class="pflForbidden"';
if(list.length == 1) {
fullHtml += '<p>' + word + ' <code'+cls+'>' + list[0] + '</code>.</p>';
} else if(list.length > 0) {
fullHtml += '<p>' + words + ' <code'+cls+'>' + list.join('</code>, <code'+cls+'>') + '</code>.</p>';
}
return list;
}
var pflAllowed = processForbiddenList(pflInfos.allowed, true);
processForbiddenList(pflInfos.forbidden, false);
if(pflAllowed.length) {
simpleHtml += '<br />' + this._strings.autorizedKeyWords + displaySimpleList(pflAllowed);
}
if(pflInfos.allowed.indexOf('var_assign') > -1) {
fullHtml += '<p>' + window.languageStrings.variablesAllowed + '</p>';
} else {
fullHtml += '<p>' + window.languageStrings.variablesForbidden + '</p>';
}
fullHtml += '<p>' + window.languageStrings.readDocumentation + '</p>';
$('.pythonIntroSimple').html(simpleHtml);
$('.pythonIntroFull').html(fullHtml);
// Display the full details in the responsive version
this.collapseTaskIntro(!window.quickAlgoResponsive);
if(window.quickAlgoResponsive) {
$('.pythonIntroBtn').hide();
}
function updateIntroSelect(elem) {
elem = $(elem);
var code = elem.find('option:selected').text();
var funcName = code.split('(')[0];
var conceptId = null;
if(window.conceptViewer) {
conceptId = window.conceptViewer.hasPythonConcept(funcName);
}
if(conceptId) {
elem.parent().find('.pythonIntroSelectBtnHelp').attr('data-concept', conceptId).show();
} else {
elem.parent().find('.pythonIntroSelectBtnHelp').hide();
}
var desc = elem.find('option:selected').attr('data-desc');
elem.parent().find('.pythonIntroSelectDesc').html(desc || "");
}
$('.pythonIntroSelect select').each(function(idx, elem) { updateIntroSelect(elem); });
$('.pythonIntroSimple code, .pythonIntroSimple option, .pythonIntroFull code').each(function() {
var elem = $(this);
var txt = elem.text();
var pIdx = txt.indexOf('+');
if(pIdx > -1) {
var newTxt = txt.substring(0, pIdx);
var code = txt.substring(pIdx+1);
} else {
var newTxt = txt;
var code = txt;
}
elem.attr('data-code', code);
elem.text(newTxt);
});
var controller = this;
$('.pythonIntroSimple code, .pythonIntroFull code').not('.pflForbidden').on('click', function() {
quickAlgoInterface.toggleLongIntro(false);
if(controller._aceEditor) {
controller._aceEditor.insert(this.getAttribute('data-code'));
controller._aceEditor.focus();
}
});
$('.pythonIntroSelectBtn.pythonIntroSelectBtnCopy').on('click', function() {
var code = $(this).parent().find('option:selected').attr('data-code');
if(controller._aceEditor) {
controller._aceEditor.insert(code);
controller._aceEditor.focus();
}
});
$('.pythonIntroSelectBtn.pythonIntroSelectBtnHelp').on('click', function() {
window.conceptViewer.showConcept($(this).attr('data-concept'));
});
$('.pythonIntroSelect select').on('change', function() {
updateIntroSelect(this);
});
};
this.collapseTaskIntro = function(collapse) {
var that = this;
var div = $('.pythonIntroBtn').html('');
if(collapse) {
$('<a>' + window.languageStrings.showDetails + '</a>').appendTo(div).on('click', function() { that.collapseTaskIntro(false); });
$('.pythonIntro .pythonIntroFull').hide();
$('.pythonIntro .pythonIntroSimple').show();
} else {
$('<a>' + window.languageStrings.hideDetails + '</a>').appendTo(div).on('click', function() { that.collapseTaskIntro(true); });
$('.pythonIntro .pythonIntroFull').show();
$('.pythonIntro .pythonIntroSimple').hide();
}
};
this.toggleSize = function () {
// Currently unused
if (!this.extended) {
this.extended = true;
$('#editorContainer').css("width", "800px");
$("#extendButton").val("<<");
} else {
this.extended = false;
$('#editorContainer').css("width", "500px");
$("#extendButton").val(">>");
}
this.updateSize();
};
this.updateSize = function () {
var panelWidth = 500;
if ($("#editorContainer").length > 0) {
panelWidth = $('#editorContainer').width() - 30;
if (panelWidth != this._prevWidth) {
$("#taskIntro").css("width", panelWidth);
$("#grid").css("left", panelWidth + 20 + "px");
}
}
this._prevWidth = panelWidth;
};
this.resetDisplay = function () {
if(this._mainContext.runner) {
this._mainContext.runner.removeEditorMarker();
}
};
this.reload = function () {};
this.setReadOnly = function(newState) {
// setReadOnly called by quickAlgoInterface
// TODO :: should we actually set the readOnly flag?
return;
if(!!newState == this._readOnly) { return; }
this._readOnly = !!newState;
// options.readOnly has priority
if(this._options.readOnly) { return; }
this._aceEditor.setOption('readOnly', this._readOnly);
};
this.canPaste = function() {
return window.pythonClipboard ? true : null;
};
this.canConvertBlocklyToPython = function() {
return false;
};
this.copyProgram = function() {
var code = this._aceEditor.getSelectedText();
if(!code) { code = this._aceEditor.getValue(); }
window.pythonClipboard = code;
};
this.pasteProgram = function() {
if(!window.pythonClipboard) { return; }
var curCode = this._aceEditor.getValue();
this._aceEditor.setValue(curCode + '\n\n' + window.pythonClipboard);
var Range = ace.require('ace/range').Range;
this._aceEditor.selection.setRange(new Range(curCode.split(/\r\n|\r|\n/).length + 1, 0, this._aceEditor.getValue().split(/\r\n|\r|\n/).length, 0), true);
};
}
function getBlocklyHelper(maxBlocks, subTask) {
return new LogicController(maxBlocks, subTask);
}