Spaces:
Running
Running
| function CodeEditor(textAreaDomID, width, height, game) { | |
| var symbols = { | |
| 'begin_line':'#BEGIN_EDITABLE#', | |
| 'end_line':'#END_EDITABLE#', | |
| 'begin_char':"#{#", | |
| 'end_char': "#}#", | |
| 'begin_properties':'#BEGIN_PROPERTIES#', | |
| 'end_properties':'#END_PROPERTIES#', | |
| 'start_start_level':'#START_OF_START_LEVEL#', | |
| 'end_start_level':'#END_OF_START_LEVEL#' | |
| }; | |
| var charLimit = 80; | |
| var properties = {}; | |
| var editableLines = []; | |
| var editableSections = {}; | |
| var lastChange = {}; | |
| var startOfStartLevel = null; | |
| var endOfStartLevel = null; | |
| this.setEndOfStartLevel = function (eosl) { | |
| endOfStartLevel = eosl; | |
| } | |
| this.setEditableLines = function (el) { | |
| editableLines = el; | |
| } | |
| this.setEditableSections = function (es) { | |
| editableSections = es; | |
| } | |
| // for debugging purposes | |
| log = function (text) { | |
| if (game._debugMode) { | |
| console.log(text); | |
| } | |
| } | |
| // preprocesses code,determines the location | |
| // of editable lines and sections, loads properties | |
| function preprocess(codeString) { | |
| editableLines = []; | |
| editableSections = {}; | |
| endOfStartLevel = null; | |
| startOfStartLevel = null; | |
| var propertiesString = ''; | |
| var lineArray = codeString.split("\n"); | |
| var inEditableBlock = false; | |
| var inPropertiesBlock = false; | |
| for (var i = 0; i < lineArray.length; i++) { | |
| var currentLine = lineArray[i]; | |
| // process properties | |
| if (currentLine.indexOf(symbols.begin_properties) === 0) { | |
| lineArray.splice(i,1); // be aware that this *mutates* the list | |
| i--; | |
| inPropertiesBlock = true; | |
| } else if (currentLine.indexOf(symbols.end_properties) === 0) { | |
| lineArray.splice(i,1); | |
| i--; | |
| inPropertiesBlock = false; | |
| } else if (inPropertiesBlock) { | |
| lineArray.splice(i,1); | |
| i--; | |
| propertiesString += currentLine; | |
| } | |
| // process editable lines and sections | |
| else if (currentLine.indexOf(symbols.begin_line) === 0) { | |
| lineArray.splice(i,1); | |
| i--; | |
| inEditableBlock = true; | |
| } else if (currentLine.indexOf(symbols.end_line) === 0) { | |
| lineArray.splice(i,1); | |
| i--; | |
| inEditableBlock = false; | |
| } | |
| // process start of startLevel() | |
| else if (currentLine.indexOf(symbols.start_start_level) === 0) { | |
| lineArray.splice(i,1); | |
| startOfStartLevel = i; | |
| i--; | |
| } | |
| // process end of startLevel() | |
| else if (currentLine.indexOf(symbols.end_start_level) === 0) { | |
| lineArray.splice(i,1); | |
| endOfStartLevel = i; | |
| i--; | |
| } | |
| // everything else | |
| else { | |
| if (inEditableBlock) { | |
| editableLines.push(i); | |
| } else { | |
| // check if there are any editable sections | |
| var sections = []; | |
| var startPoint = null; | |
| for (var j = 0; j < currentLine.length - 2; j++) { | |
| if (currentLine.slice(j,j+3) === symbols.begin_char) { | |
| currentLine = currentLine.slice(0,j) + currentLine.slice(j+3, currentLine.length); | |
| startPoint = j; | |
| } else if (currentLine.slice(j,j+3) === symbols.end_char) { | |
| currentLine = currentLine.slice(0,j) + currentLine.slice(j+3, currentLine.length); | |
| sections.push([startPoint, j]); | |
| } | |
| } | |
| if (sections.length > 0) { | |
| lineArray[i] = currentLine; | |
| editableSections[i] = sections; | |
| } | |
| } | |
| } | |
| } | |
| try { | |
| properties = JSON.parse(propertiesString); | |
| } catch (e) { | |
| properties = {}; | |
| } | |
| return lineArray.join("\n"); | |
| } | |
| var findEndOfSegment = function(line) { | |
| // Given an editable line number, returns the last line of the | |
| // given line's editable segment. | |
| if (editableLines.indexOf(line + 1) === -1) { | |
| return line; | |
| } | |
| return findEndOfSegment(line + 1); | |
| }; | |
| var shiftLinesBy = function(array, after, shiftAmount) { | |
| // Shifts all line numbers strictly after the given line by | |
| // the provided amount. | |
| return array.map(function(line) { | |
| if (line > after) { | |
| log('Shifting ' + line + ' to ' + (line + shiftAmount)); | |
| return line + shiftAmount; | |
| } | |
| return line; | |
| }); | |
| }; | |
| // enforces editing restrictions when set as the handler | |
| // for the 'beforeChange' event | |
| var enforceRestrictions = function(instance, change) { | |
| lastChange = change; | |
| var inEditableArea = function(c) { | |
| var lineNum = c.to.line; | |
| if (editableLines.indexOf(lineNum) !== -1 && editableLines.indexOf(c.from.line) !== -1) { | |
| // editable lines? | |
| return true; | |
| } else if (editableSections[lineNum]) { | |
| // this line has editable sections - are we in one of them? | |
| var sections = editableSections[lineNum]; | |
| for (var i = 0; i < sections.length; i++) { | |
| var section = sections[i]; | |
| if (c.from.ch > section[0] && c.to.ch > section[0] && | |
| c.from.ch < section[1] && c.to.ch < section[1]) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| }; | |
| log( | |
| '---Editor input (beforeChange) ---\n' + | |
| 'Kind: ' + change.origin + '\n' + | |
| 'Number of lines: ' + change.text.length + '\n' + | |
| 'From line: ' + change.from.line + '\n' + | |
| 'To line: ' + change.to.line | |
| ); | |
| if (!inEditableArea(change)) { | |
| change.cancel(); | |
| } else if (change.to.line < change.from.line || | |
| change.to.line - change.from.line + 1 > change.text.length) { // Deletion | |
| updateEditableLinesOnDeletion(change); | |
| } else { // Insert/paste | |
| // First line already editable | |
| var newLines = change.text.length - (change.to.line - change.from.line + 1); | |
| if (newLines > 0) { | |
| if (editableLines.indexOf(change.to.line) < 0) { | |
| change.cancel(); | |
| return; | |
| } | |
| // enforce 80-char limit by wrapping all lines > 80 chars | |
| var wrappedText = []; | |
| change.text.forEach(function (line) { | |
| while (line.length > charLimit) { | |
| // wrap lines at spaces if at all possible | |
| var minCutoff = charLimit - 20; | |
| var cutoff = minCutoff + line.slice(minCutoff).indexOf(" "); | |
| if (cutoff > 80) { | |
| // no suitable cutoff point found - let's get messy | |
| cutoff = 80; | |
| } | |
| wrappedText.push(line.substr(0, cutoff)); | |
| line = line.substr(cutoff); | |
| } | |
| wrappedText.push(line); | |
| }); | |
| change.text = wrappedText; | |
| // updating line count | |
| newLines = change.text.length - (change.to.line - change.from.line + 1); | |
| updateEditableLinesOnInsert(change, newLines); | |
| } else { | |
| // enforce 80-char limit by trimming the line | |
| var lineLength = instance.getLine(change.to.line).length; | |
| if (lineLength + change.text[0].length > charLimit) { | |
| var allowedLength = Math.max(charLimit - lineLength, 0); | |
| change.text[0] = change.text[0].substr(0, allowedLength); | |
| } | |
| } | |
| // modify editable sections accordingly | |
| // TODO Probably broken by multiline paste | |
| var sections = editableSections[change.to.line]; | |
| if (sections) { | |
| var delta = change.text[0].length - (change.to.ch - change.from.ch); | |
| for (var i = 0; i < sections.length; i++) { | |
| // move any section start/end points that we are to the left of | |
| if (change.to.ch < sections[i][1]) { | |
| sections[i][1] += delta; | |
| } | |
| if (change.to.ch < sections[i][0]) { | |
| sections[i][0] += delta; | |
| } | |
| } | |
| } | |
| } | |
| log(editableLines); | |
| } | |
| var updateEditableLinesOnInsert = function(change, newLines) { | |
| var lastLine = findEndOfSegment(change.to.line); | |
| // Shift editable line numbers after this segment | |
| editableLines = shiftLinesBy(editableLines, lastLine, newLines); | |
| // TODO If editable sections appear together with editable lines | |
| // in a level, multiline edit does not properly handle editable | |
| // sections. | |
| log("Appending " + newLines + " lines"); | |
| // Append new lines | |
| for (var i = lastLine + 1; i <= lastLine + newLines; i++) { | |
| editableLines.push(i); | |
| } | |
| // Update endOfStartLevel | |
| if (endOfStartLevel) { | |
| endOfStartLevel += newLines; | |
| } | |
| }; | |
| var updateEditableLinesOnDeletion = function(changeInput) { | |
| // Figure out how many lines just got removed | |
| var numRemoved = changeInput.to.line - changeInput.from.line - changeInput.text.length + 1; | |
| // Find end of segment | |
| var editableSegmentEnd = findEndOfSegment(changeInput.to.line); | |
| // Remove that many lines from its end, one by one | |
| for (var i = editableSegmentEnd; i > editableSegmentEnd - numRemoved; i--) { | |
| log('Removing\t' + i); | |
| editableLines.remove(i); | |
| } | |
| // Shift lines that came after | |
| editableLines = shiftLinesBy(editableLines, editableSegmentEnd, -numRemoved); | |
| // TODO Shift editableSections | |
| // Update endOfStartLevel | |
| if (endOfStartLevel) { | |
| endOfStartLevel -= numRemoved; | |
| } | |
| }; | |
| // beforeChange events don't pick up undo/redo | |
| // so we track them on change event | |
| var trackUndoRedo = function(instance, change) { | |
| if (change.origin === 'undo' || change.origin === 'redo') { | |
| enforceRestrictions(instance, change); | |
| } | |
| } | |
| this.initialize = function() { | |
| this.internalEditor = CodeMirror.fromTextArea(document.getElementById(textAreaDomID), { | |
| theme: 'vibrant-ink', | |
| lineNumbers: true, | |
| dragDrop: false, | |
| smartIndent: false | |
| }); | |
| this.internalEditor.setSize(width, height); | |
| // set up event handlers | |
| this.internalEditor.on("focus", function(instance) { | |
| // implements yellow box when changing focus | |
| $('.CodeMirror').addClass('focus'); | |
| $('#screen canvas').removeClass('focus'); | |
| $('#helpPane').hide(); | |
| $('#menuPane').hide(); | |
| }); | |
| this.internalEditor.on('cursorActivity', function (instance) { | |
| // fixes the cursor lag bug | |
| instance.refresh(); | |
| // automatically smart-indent if the cursor is at position 0 | |
| // and the line is empty (ignore if backspacing) | |
| if (lastChange.origin !== '+delete') { | |
| var loc = instance.getCursor(); | |
| if (loc.ch === 0 && instance.getLine(loc.line).trim() === "") { | |
| instance.indentLine(loc.line, "prev"); | |
| } | |
| } | |
| }); | |
| this.internalEditor.on('change', markEditableSections); | |
| this.internalEditor.on('change', trackUndoRedo); | |
| } | |
| // loads code into editor | |
| this.loadCode = function(codeString) { | |
| /* | |
| * logic: before setting the value of the editor to the code string, | |
| * we run it through setEditableLines and setEditableSections, which | |
| * strip our notation from the string and as a side effect build up | |
| * a data structure of editable areas | |
| */ | |
| this.internalEditor.off('beforeChange', enforceRestrictions); | |
| codeString = preprocess(codeString); | |
| this.internalEditor.setValue(codeString); | |
| this.internalEditor.on('beforeChange', enforceRestrictions); | |
| this.markUneditableLines(); | |
| this.internalEditor.refresh(); | |
| this.internalEditor.clearHistory(); | |
| }; | |
| // marks uneditable lines within editor | |
| this.markUneditableLines = function() { | |
| var instance = this.internalEditor; | |
| for (var i = 0; i < instance.lineCount(); i++) { | |
| if (editableLines.indexOf(i) === -1) { | |
| instance.addLineClass(i, 'wrap', 'disabled'); | |
| } | |
| } | |
| } | |
| // marks editable sections inside uneditable lines within editor | |
| var markEditableSections = function(instance) { | |
| $('.editableSection').removeClass('editableSection'); | |
| for (var line in editableSections) { | |
| if (editableSections.hasOwnProperty(line)) { | |
| var sections = editableSections[line]; | |
| for (var i = 0; i < sections.length; i++) { | |
| var section = sections[i]; | |
| var from = {'line': parseInt(line), 'ch': section[0]}; | |
| var to = {'line': parseInt(line), 'ch': section[1]}; | |
| instance.markText(from, to, {'className': 'editableSection'}); | |
| } | |
| } | |
| } | |
| } | |
| // returns all contents | |
| this.getCode = function (forSaving) { | |
| var lines = this.internalEditor.getValue().split('\n'); | |
| if (!forSaving && startOfStartLevel) { | |
| // insert the end of startLevel() marker at the appropriate location | |
| lines.splice(startOfStartLevel, 0, "map._startOfStartLevelReached()"); | |
| } | |
| if (!forSaving && endOfStartLevel) { | |
| // insert the end of startLevel() marker at the appropriate location | |
| lines.splice(endOfStartLevel+1, 0, "map._endOfStartLevelReached()"); | |
| } | |
| return lines.join('\n'); | |
| } | |
| // returns only the code written in editable lines and sections | |
| this.getPlayerCode = function () { | |
| var code = ''; | |
| for (var i = 0; i < this.internalEditor.lineCount(); i++) { | |
| if (editableLines && editableLines.indexOf(i) > -1) { | |
| code += this.internalEditor.getLine(i) + ' \n'; | |
| } | |
| } | |
| for (var line in editableSections) { | |
| if (editableSections.hasOwnProperty(line)) { | |
| var sections = editableSections[line]; | |
| for (var i = 0; i < sections.length; i++) { | |
| var section = sections[i]; | |
| code += this.internalEditor.getLine(line).slice(section[0], section[1]) + ' \n'; | |
| } | |
| } | |
| } | |
| return code; | |
| }; | |
| this.getProperties = function () { | |
| return properties; | |
| } | |
| this.setCode = function(code) { | |
| // make sure we're not saving the hidden START/END_OF_START_LEVEL lines | |
| code = code.split('\n').filter(function (line) { | |
| return line.indexOf('OfStartLevelReached') < 0; | |
| }).join('\n'); | |
| this.internalEditor.off('beforeChange',enforceRestrictions); | |
| this.internalEditor.setValue(code); | |
| this.internalEditor.on('beforeChange', enforceRestrictions); | |
| this.markUneditableLines(); | |
| this.internalEditor.refresh(); | |
| this.internalEditor.clearHistory(); | |
| } | |
| this.saveGoodState = function () { | |
| var lvlNum = game._currentFile ? game._currentFile : game._currentLevel; | |
| localStorage.setItem(game._getLocalKey('level' + lvlNum + '.lastGoodState'), JSON.stringify({ | |
| code: this.getCode(true), | |
| playerCode: this.getPlayerCode(), | |
| editableLines: editableLines, | |
| editableSections: editableSections, | |
| endOfStartLevel: endOfStartLevel, | |
| version: this.getProperties().version | |
| })); | |
| } | |
| this.createGist = function () { | |
| var lvlNum = game._currentLevel; | |
| var filename = 'untrusted-lvl' + lvlNum + '-solution.js'; | |
| var description = 'Solution to level ' + lvlNum + ' in Untrusted: http://alex.nisnevich.com/untrusted/'; | |
| var data = { | |
| 'files': {}, | |
| 'description': description, | |
| 'public': true | |
| }; | |
| data['files'][filename] = { | |
| 'content': this.getCode(true).replace(/\t/g, ' ') | |
| }; | |
| var t = ['372f2dad', '3edbb23c', '7c82f871', '36a67eb8', '623e8b32']; | |
| $.ajax({ | |
| 'url': 'https://api.github.com/gists', | |
| 'type': 'POST', | |
| 'data': JSON.stringify(data), | |
| 'headers': { 'Authorization': 'token ' + t.join('') }, | |
| 'success': function (data, status, xhr) { | |
| $('#savedLevelMsg').html('Level ' + lvlNum + ' solution saved at <a href="' | |
| + data['html_url'] + '" target="_blank">' + data['html_url'] + '</a>'); | |
| } | |
| }); | |
| } | |
| this.getGoodState = function (lvlNum) { | |
| return JSON.parse(localStorage.getItem(game._getLocalKey('level' + lvlNum + '.lastGoodState'))); | |
| } | |
| this.refresh = function () { | |
| this.internalEditor.refresh(); | |
| } | |
| this.focus = function () { | |
| this.internalEditor.focus(); | |
| } | |
| this.initialize(); // run initialization code | |
| } | |