From 21d213dcff1367c16dc0c3f6585b8e35d7c2f0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Smodi=C5=A1?= Date: Mon, 24 Aug 2015 19:17:43 +0200 Subject: Introduced the Q promises library, created a basic login page to test prolog examples, and started tying the terminal and editor activity to the REST services. --- js/codeq.js | 83 ++- js/codeq/comms.js | 64 ++ js/codeq/core.js | 33 + js/codeq/login.js | 183 +++++ js/prolog.js | 122 +++- js/q.js | 2051 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 2516 insertions(+), 20 deletions(-) create mode 100644 js/codeq/comms.js create mode 100644 js/codeq/core.js create mode 100644 js/codeq/login.js create mode 100644 js/q.js (limited to 'js') diff --git a/js/codeq.js b/js/codeq.js index 472cab0..9e33bac 100644 --- a/js/codeq.js +++ b/js/codeq.js @@ -80,6 +80,8 @@ window.phandler = null; // TODO: this is for debug only }; } + codeq.jsonize = jsonize; + codeq.log = {}; (function () { var assembleOutput = function (stuff, e) { @@ -706,7 +708,23 @@ window.phandler = null; // TODO: this is for debug only }; // codeq.system = { - window.start = function () { + /** + * Returns the number of Unicode code points in the given string. + * + * @param s {string} + * @returns {number} + */ + codeq.codePointCount = function (s) { + var n = 0, i, code; + for (i = s.length - 1; i >= 0; i--) { + code = s.charCodeAt(i); + if ((code >= 0xd800) && (code < 0xe000)) i++; + n++; + } + return n; + }; + + $(document).ready(function () { // var s = location.hash; // if (s.length == 0) return; // empty hash // if (s.charAt(0) == '#') s = s.substring(1); @@ -724,17 +742,56 @@ window.phandler = null; // TODO: this is for debug only width: '100%' });*/ - codeq.system.load({ - type: 'text', - url: 'sins.py', - callback: function (data, status, url) { - if (!data) return; - var info = codeq.parseDefinition(data); - window.phandler = codeq.createPrologHandler(info); // TODO: for debug only - // DEBUG: phandler.processServerHints([{id:'x_must_be_female'}]); - // DEBUG: phandler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); - // DEBUG: phandler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); + var hash = location.hash, + error = false, + params, param, sid, grp, prb, i, key, j; + + if (hash.length < 2) { + error = 'No execution parameters were provided'; + } + else { + location.hash = ''; + if (hash.charAt(0) == '#') hash = hash.substring(1); + params = hash.split('/'); + for (i = params.length - 1; i >= 0; i--) { + param = params[i]; + j = param.indexOf('='); + if (j < 0) continue; + key = param.substring(0, j); + switch (key) { + case 'sid': + sid = param.substring(j+1); + break; + case 'grp': + grp = param.substring(j+1); + break; + case 'prb': + prb = param.substring(j+1); + break; + } } - }); - }; + if (!sid) error = 'No session ID provided'; + else if (!grp) error = 'No problem group provided'; + else if (!prb) error = 'No problem provided'; + } + + if (error) { + alert(error); + } + else { + codeq.sid = sid; + codeq.system.load({ + type: 'text', + url: 'prolog/problems/' + grp + '/' + prb + '/en.py', + callback: function (data, status, url) { + if (!data) return; + var info = codeq.parseDefinition(data); + window.phandler = codeq.createPrologHandler(info, grp, prb); // TODO: for debug only + // DEBUG: phandler.processServerHints([{id:'x_must_be_female'}]); + // DEBUG: phandler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); + // DEBUG: phandler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); + } + }); + } + }); })(); diff --git a/js/codeq/comms.js b/js/codeq/comms.js new file mode 100644 index 0000000..fb5d348 --- /dev/null +++ b/js/codeq/comms.js @@ -0,0 +1,64 @@ +(function () { + + var activityQueue = []; + + var send = function (service, json) { + return Q.Promise(function (resolve, reject, notify) { + $.ajax({ + 'type': 'POST', + 'url': codeq.urlPrefix + service, + 'accepts': 'application/json', + 'contentType': 'application/json; charset=UTF-8', // type of our request + 'data': json, + 'processData': false, // don't process outgoing data + 'dataType': 'json', // expected type of the response + 'timeout': 60000, // one minute + 'error': function sendErrorHandler(jqXHR, textStatus, errorThrown) { + reject(new Error(errorThrown || textStatus)); + }, + 'success': function sendSuccessHandler(data, textStatus, jqXHR) { + resolve(data); + } + }); + }); + }; + + var sendCount = 0, + sendActivityInternal = function () { + var request; + // send max. 100 activities, do not be excessive + if (activityQueue.length > 100) { + sendCount = 100; + request = '[' + activityQueue.slice(0, 100).join(',') + ']'; + } + else { + sendCount = activityQueue.length; + request = '[' + activityQueue.join(',') + ']'; + } + send('activity', request).then( + function sendActivitySuccess() { + activityQueue.splice(0, sendCount); + if (activityQueue.length > 0) sendActivityInternal(); + }, + function sendActivityFailure() { + Q.delay(500).then(sendActivityInternal).done(); + } + ).done(); + }; + + codeq.comms = { + sendActivity: function commsSendActivity (json) { + var triggerSending = activityQueue.length == 0; + json['sid'] = codeq.sid; + activityQueue.push(codeq.jsonize(json)); + if (triggerSending) { + setTimeout(sendActivityInternal, 0); // async trigger: see if you can collect some more payload + } + }, + + sendQuery: function commsSendQuery (json) { + json['sid'] = codeq.sid; + return send('query', codeq.jsonize(json)); + } + }; +})(); diff --git a/js/codeq/core.js b/js/codeq/core.js new file mode 100644 index 0000000..f13a019 --- /dev/null +++ b/js/codeq/core.js @@ -0,0 +1,33 @@ +window.siteDefinition = { logLevel: 'debug' }; // for debug purposes + +window.codeq = { + /** + * XML namespaces. + */ + ns: { + svg: 'http://www.w3.org/2000/svg' + }, + + noOnlineStatus: !('onLine' in navigator), + + /** + * REST API URL prefix. + */ + urlPrefix: '/codeq/', + + /** + * Returns the number of Unicode code points in the given string. + * + * @param s {string} + * @returns {number} + */ + codePointCount: function (s) { + var n = 0, i, code; + for (i = s.length - 1; i >= 0; i--) { + code = s.charCodeAt(i); + if ((code >= 0xd800) && (code < 0xe000)) i++; + n++; + } + return n; + } +}; diff --git a/js/codeq/login.js b/js/codeq/login.js new file mode 100644 index 0000000..d449835 --- /dev/null +++ b/js/codeq/login.js @@ -0,0 +1,183 @@ +(function(){ + var groups = { + 'family_relations': { + 'name': 'Family relations', + 'problems': [ + {'id': 'ancestor_2', 'name': 'ancestor/2'}, + {'id': 'aunt_2', 'name': 'aunt/2'}, + {'id': 'brother_2', 'name': 'brother/2'}, + {'id': 'connected_3', 'name': 'connected/3'}, + {'id': 'cousin_2', 'name': 'cousin/2'} + ] + }, + 'lists': { + 'name': 'Lists', + 'problems': [ + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''} + ] + }, + 'sorting': { + 'name': 'Sorting', + 'problems': [ + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''} + ] + }, + 'other': { + 'name': 'Other', + 'problems': [ + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''} + ] + }, + 'sets': { + 'name': 'Sets', + 'problems': [ + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''} + ] + }, + 'trees': { + 'name': 'Trees', + 'problems': [ + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''} + ] + }, + 'license_plates': { + 'name': 'License plates', + }, + 'clp_fd': { + 'name': 'CLP(FD', + 'problems': [ + {'id': 'gcd_3', 'name': 'gcd/3'}, + {'id': 'magic_1', 'name': 'magic/1'}, + {'id': 'puzzle_abc_3', 'name': 'puzzle_abc/3'}, + {'id': 'puzzle_beth_1', 'name': 'puzzle_beth/1'}, + {'id': 'puzzle_momson_2', 'name': 'puzzle_momson/2'}, + {'id': 'puzzle_ratio_2', 'name': 'puzzle_ratio/2'}, + {'id': 'tobase_3', 'name': 'tobase/3'} + ] + }, + 'clp_r': { + 'name': 'CLP(R)', + 'problems': [ + {'id': 'bounding_box_3', 'name': 'bounding_box/3'}, + {'id': 'center_3', 'name': 'center/3'}, + {'id': 'linear_opts_3', 'name': 'linear_opts/3'}, + {'id': 'max_sum_2', 'name': 'max_sum/2'}, + {'id': 'megabytes_2', 'name': 'megabytes/2'}, + {'id': 'turkey_3', 'name': 'turkey/3'} + ] + }, + 'dcg': { + 'name': 'DCG', + 'problems': [ + {'id': 'ab_2', 'name': 'ab/2'}, + {'id': 'digit_2', 'name': 'digit/2'}, + {'id': 'expr_2', 'name': 'expr/2'}, + {'id': 'expr_3', 'name': 'expr/3'}, + {'id': 'flower_2', 'name': 'flower/2'} + ] + }, + 'denotational_semantics': { + 'name': 'Denotational semantics', + 'problems': [ + {'id': 'algol_3', 'name': 'algol/3'}, + {'id': 'algol_for_3', 'name': 'algol_for/3'}, + {'id': 'algol_if_3', 'name': 'algol_if/3'}, + {'id': 'prog_8puzzle_2', 'name': 'prog_8puzzle/2'}, + {'id': 'prog_8puzzle_3', 'name': 'prog_8puzzle/3'} + ] + }, + 'old_exams': { + 'name': 'Old exams', + 'problems': [ + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''}, + {'id': '', 'name': ''} + ] + } + }; + + $("#submit").on('click', function () { + var group = $('#problem_group').val(), + problem = $('#problems').val(); + if (!group) alert('Choose a problem group'); + else if (!problem) alert('Choose a problem'); + else { + $.ajax({ + 'type': 'POST', + 'url': '/codeq/login', + 'accepts': 'application/json', + 'contentType': 'application/json; charset=UTF-8', + 'data': JSON.stringify({ + 'username': $('#username').val(), + 'password': $('#password').val() + }), + 'processData': false, + 'dataType': 'json', + 'error': function loginErrorHandler(jqXHR, textStatus, errorThrown) { + alert('Request for login failed: ' + (errorThrown || textStatus)); + }, + 'success': function loginSuccessHandler(data, textStatus, jqXHR) { + if (data && (data.code === 0)) { + window.location = 'prolog.html#sid=' + data.sid + '/grp=' + group + '/prb=' + problem; + } + else { + alert('Login failed: code=' + data.code + ', reason=' + data.message); + } + } + }); + } + }); + + $(document).ready(function () { + var jqGroup = $('#problem_group'), + jqProblems = $('#problems'), + id, g, first_group, html = []; + + for (id in groups) { + g = groups[id]; + html.push('\n'); + } + jqGroup.html(html.join('')); + first_group = html[1]; + html = null; + + jqGroup.on('click', function () { + var g = groups[jqGroup.val()], + html = [], + problems, i, p; + if (g) { + problems = g.problems; + for (i = 0; i < problems.length; i++) { + p = problems[i]; + if (!p.id) continue; + html.push('\n') + } + } + jqProblems.html(html.join('')); + }); + + jqGroup.val(first_group); + }); +})(); \ No newline at end of file diff --git a/js/prolog.js b/js/prolog.js index 28c1b69..0a0eb48 100644 --- a/js/prolog.js +++ b/js/prolog.js @@ -3,19 +3,95 @@ */ (function () { + // a constant + var firstCharacterPos = {'line': 0, 'ch': 0}; + + var makePrologTerminalHandler = function (jqConsole, editor, problem_group, problem) { + var promptMode = true, // default: query composition; alternative: query result browsing + tcs = function terminalCommandSuccess (data) { + var t, lines, i; + terminal.resume(); + if (data.code === 0) { + t = data.terminal; + lines = t.messages; + for (i = 0; i < lines.length; i++) { + terminal.echo(lines[i]); + } + promptMode = !t.have_more; + } + else { + terminal.error(data.message); + promptMode = true; + } + }, + tcf = function terminalCommandFailed (error) { + terminal.resume(); + terminal.exception(error); + promptMode = true; + }, + terminal = jqConsole.terminal(function (command, terminal) { + if (promptMode) { + terminal.pause(); + codeq.comms.sendQuery({ + 'step': 'run', + 'program': editor.getDoc().getValue(), + 'query': command, + 'language': 'prolog', + 'problem_group': problem_group, + 'problem': problem + }).then(tcs, tcf).done(); + } + else { + // not in prompt mode -- we should never land here, but handle it anyway + codeq.comms.sendQuery({ + 'step': 'end' + }).then(tcs, tcf).done(); + } + }, { + 'history': true, + 'prompt': '?- ', + 'greetings': 'CodeQ prolog terminal proxy', + 'exit': false, + 'clear': false, + 'keypress': function (event, terminal) { + if (promptMode) return true; + setTimeout(function () { + terminal.echo(''); // send newline after semicolon or full-stop + terminal.pause(); + }, 0); + if ((event.which == 32) || (event.which == 59)) { + // space or semicolon -> show next answer + event.which = 59; // semicolon + codeq.comms.sendQuery({ + 'step': 'next' + }).then(tcs, tcf).done(); + } + else { + // everything else: stop searching for answers + event.which = 46; // full stop + codeq.comms.sendQuery({ + 'step': 'end' + }).then(tcs, tcf).done(); + } + } + }); + + return {}; + }; + /** * Creates a new handler for the given Prolog assignment definition. * * @param {PrologTaskDef} info * @returns {{destroy: Function, processServerHints: Function}} */ - codeq.createPrologHandler = function (info) { + codeq.createPrologHandler = function (info, problem_group, problem) { var jqDescription = $('#description'), jqEditor = $('#code_editor'), jqConsole = $('#console'), jqHints = $('#info'), editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true }), - controller = jqConsole.console({ +/* controller = jqConsole.console({ promptLabel: '?- ', commandValidate: function (line) { return !!line; @@ -27,7 +103,9 @@ animateScroll: false, promptHistory: false, welcomeMessage: 'Prolog REPL.' - }), /** Object. */ + }),*/ + terminal = makePrologTerminalHandler(jqConsole, editor, problem_group, problem), + /** Object. */ hintDefs = info.hint, hintCounter = 0, // for generating unique class-names hintCleaners = [], @@ -112,7 +190,7 @@ }; if ((editor.listSelections().length > 1) || editor.somethingSelected()) { - // showHint() doesn't work if a selection is active + // showHint() doesn't work if a selection is activeparts } editor.showHint({ @@ -131,11 +209,32 @@ hintCleaners.push(close); } + }, + lastActivityMillis = Date.now(), + deltaActivityMillis = function deltaActivityMillisFunc () { + var now = Date.now(), + dt = Math.max(0, Math.min(30000, now - lastActivityMillis)); // 0 sec <= dt <= 30 sec + lastActivityMillis = now; + return dt; }; editor.setValue('sister(X, Y) :-\n female(X),\n parent(Z, X),\n parent(Z, Y),\n X \\== Y.'); // demo jqDescription.html(info.description); + editor.on('change', function (instance, changeObj) { + var doc = editor.getDoc(), + pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from)), + dt = deltaActivityMillis(); + + if (changeObj.removed) { +// codeq.comms.sendActivity({'typ': 'rm', 'dt': dt, 'off': pos, 'len': codeq.codePointCount(changeObj.removed)}); + } + + if (changeObj.text) { +// codeq.comms.sendActivity({'typ': 'ins', 'dt': dt, 'off': pos, 'txt': changeObj.text}); + } + }); + var handler = { destroy: function () { jqDescription.empty(); @@ -194,9 +293,18 @@ // $(jqButtons.get(1)).on('click', function () { handler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); }); // $(jqButtons.get(2)).on('click', function () { handler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); }); - $(jqButtons.get(0)).on('click', function () { handler.processServerHints([{id:'list_empty'}]); }); - $(jqButtons.get(1)).on('click', function () { handler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); }); - $(jqButtons.get(2)).on('click', function () { handler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); }); + $('#btn_code_run').on('click', function () { + var doc = editor.getDoc(), + dt = deltaActivityMillis(); +// codeq.comms.sendActivity({'typ': 'slv', 'dt': dt, 'qry': }); + handler.processServerHints([{id:'list_empty'}]); + }); + $('#btn_code_break').on('click', function () { + handler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); + }); + $('#btn_code_hint').on('click', function () { + handler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); + }); return handler; }; diff --git a/js/q.js b/js/q.js new file mode 100644 index 0000000..adaca8c --- /dev/null +++ b/js/q.js @@ -0,0 +1,2051 @@ +// vim:ts=4:sts=4:sw=4: +/*! + * + * Copyright 2009-2012 Kris Kowal under the terms of the MIT + * license found at http://github.com/kriskowal/q/raw/master/LICENSE + * + * With parts by Tyler Close + * Copyright 2007-2009 Tyler Close under the terms of the MIT X license found + * at http://www.opensource.org/licenses/mit-license.html + * Forked at ref_send.js version: 2009-05-11 + * + * With parts by Mark Miller + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +(function (definition) { + "use strict"; + + // This file will function properly as a