/* Part of SWISH Author: Jan Wielemaker E-mail: J.Wielemaker@cs.vu.nl WWW: http://www.swi-prolog.org Copyright (C): 2014-2019, VU University Amsterdam CWI Amsterdam All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @fileOverview * Run an manage Prolog queries and their output * * @version 0.2.0 * @author Jan Wielemaker, J.Wielemaker@vu.nl * @requires jquery * @requires laconic * @requires editor */ define(["jquery", "config", "preferences", "utils", "cm/lib/codemirror", "form", "prolog", "links", "modal", "backend", "answer", "laconic", "sparkline", "download", "search" ], function($, config, preferences, utils, CodeMirror, form, prolog, links, modal, backend) { /******************************* * THE COLLECTION * *******************************/ (function($) { var pluginName = 'prologRunners'; /** @lends $.fn.prologRunners */ var methods = { /** * Initialize the container for Prolog queries. * @example $(".prolog-runners").prologRunners(); * @param {Object} [options] currently ignored */ _init: function(options) { return this.each(function() { var elem = $(this); var data = {}; function runnerMenu() { var icon = $.el.span({ class: "glyphicon glyphicon-menu-hamburger" }); var actions = { "Collapse all": function() { this.find(".prolog-runner").prologRunner('toggleIconic', true); }, "Expand all": function() { this.find(".prolog-runner").prologRunner('toggleIconic', false); }, "Stop all": function() { this.find(".prolog-runner").prologRunner('stop'); }, "Stop or abort all": function() { this.find(".prolog-runner").prologRunner('stopOrAbort'); }, "Clear": function() { this.prologRunners('clear'); } }; if (true || config.swish.tasks && config.swish.tasks.enabled) { actions["--"] = "divider", actions["List detached tasks"] = function() { this.prologRunners("list_tasks"); } actions["Re-attach all"] = function() { this.prologRunners("reattach"); } } var menu = form.widgets.dropdownButton( icon, { divClass: "runners-menu btn-transparent", ulClass: "pull-right", client: elem, actions: actions }); return menu; } data.stretch = $($.el.div({ class: "stretch" })); data.inner = $($.el.div({ class: "inner" })); elem.append(runnerMenu()); elem.append(data.stretch); elem.append(data.inner); elem.on("pane.resize", function() { elem.prologRunners('scrollToBottom', true); }); elem.on("scroll-to-bottom", function(ev, arg) { elem.prologRunners('scrollToBottom', arg); }); elem.data(pluginName, data); }); }, /** * Run a Prolog query. The methods appends a `
" + options.message + ""; options.location.file = true; $(data.query.query_editor).prologEditor('highlightError', options); return this; }, /** * Add an error message to the output. The error is * wrapped in a `
` element. * @param {String|Object} options If `options` is a string, it is a * plain-text error message. Otherwise it is the Pengine error * object. * @param {String} options.message is the plain error message * @param {String} options.code is the error code */ error: function(options) { var msg; var ishtml = false; if (typeof(options) == 'object') { if (options.code == "died") { addAnswer(this, $.el.div({ class: "RIP", title: "Remote pengine timed out" })); return this; } else if (options.code == "syntax_error") { var msg = options.message || options.data; var m = msg.match(/^HTTP:DATA:(\d+):(\d+):\s*(.*)/); if (m && m.length == 4) { this.prologRunner('syntaxError', { location: { line: parseInt(m[1]) - 1, ch: parseInt(m[2]) }, message: m[3] }); msg = "Cannot run query due to a syntax error (check query window)"; } } if (!msg) { if (options.html) { msg = options.html; ishtml = true; } else { msg = options.message; } } } else { msg = options; options = {}; } if (ishtml) { var el = $.el.pre({ class: "prolog-message msg-error" }, ""); $(el).append(msg); addAnswer(this, el); if (options.econtext) { $(el).addClass("error-context") .on("click", gotoError) .data("error_context", options.econtext); } } else { addAnswer(this, $.el.pre({ class: "prolog-message msg-error" }, msg)); } return this; }, /** * Handle trace events */ trace: function(data) { var elem = this; var goal = $.el.span({ class: "goal" }); var prompt = data.data; $(goal).html(prompt.goal); function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } function button(label, action, context) { var btn = $.el.button({ class: action, title: label }, $.el.span(label)); $(btn).on("click", function(ev) { if (context !== undefined) { action += "(" + Pengine.stringify(context(ev)) + ")"; } data.pengine.respond(action); $(ev.target).parent().remove(); }); return btn; } addAnswer(this, $.el.div({ class: "prolog-trace" }, $.el.span({ class: "depth", style: "width:" + (prompt.depth * 5 - 1) + "px" }, "\u00A0"), /* */ $.el.span({ class: "port " + prompt.port }, capitalizeFirstLetter(prompt.port), ":"), goal)); if (prompt.port == "exception") addAnswer(this, $.el.div({ class: "prolog-exception" }, prompt.exception.message)); addAnswer(this, $.el.div({ class: "trace-buttons" }, button("Continue", "nodebug", function(ev) { return breakpoints($(ev.target) .closest(".prolog-runner")); }), button("Step into", "continue"), button("Step over", "skip"), button("Step out", "up"), button("Retry", "retry"), button("Abort", "abort"))); this.closest(".swish") .find(".tabbed") .trigger("trace-location", prompt); this.prologRunner('setState', "wait-debug"); }, /** * set the placeholder of the input field. This is normally * done from the pengine's onprompt handler * @param {String} p the new placeholder */ setPrompt: function(p) { this.find(".controller input").attr("placeholder", p); }, /** * Support arbitrary jQuery requests from Prolog */ jQuery: function(prompt) { var request = prompt.data; var receiver; console.log(request); if (typeof(request.selector) == "string") { receiver = $(request.selector); } else if (typeof(request.selector) == "object") { switch (request.selector.root) { case "this": root = this; break; case "cell": root = this.closest(".nb-cell"); break; case "notebook": root = this.closest(".notebook"); break; case "swish": root = this.closest(".swish"); break; } if (request.selector.sub == "") { receiver = root; } else { receiver = root.find(request.selector.sub); } } var result = receiver[request.method].apply(receiver, request.arguments); prompt.pengine.respond(Pengine.stringify(result)); }, /** * Handle a (dashboard) form. This opens dialog from the supplied * `html`. * @param {Object} prompt * @param {String} prompt.html contains the HTML content of the form */ form: function(prompt) { var data = this.data('prologRunner'); modal.show({ title: "Please enter parameters", body: function() { this.html(prompt.data.html); this.find("[data-search-in]").search({ search: false }); this.on("click", "button[data-action]", function(ev) { var button = $(ev.target).closest("button"); var action = button.data('action'); if (action == 'run') { var formel = $(ev.target).closest("form"); var fdata = form.serializeAsObject(formel, true); var s = Pengine.stringify(fdata); data.prolog.respond(s); } else if (action == 'cancel') { data.prolog.respond("cancel"); } button.closest(".modal").modal('hide'); ev.preventDefault(); return false; }); } }); }, /** * send a response (to pengine onprompt handler) to the * pengine and add the response to the dialogue as * `div class="response">` * @param {String} s plain-text response */ respond: function(text) { var data = this.data('prologRunner'); if (data.wait_for == "term") { s = termNoFullStop(text); if (s == "") return null; } else { s = Pengine.stringify(text + "\n"); } addAnswer(this, $.el.div({ class: "response" }, text)); data.prolog.respond(s); return this; }, /** * Stop the associated Prolog engines. */ stop: function() { return this.each(function() { var elem = $(this); var data = elem.data('prologRunner'); data.prolog.stop(); }); }, /** * Stop the pengine if it is waiting for a next solution, * abort it if it is running or waitin for input and ignore * otherwise. */ stopOrAbort: function() { return this.each(function() { var elem = $(this); var data = elem.data('prologRunner'); var state = elem.prologRunner('getState'); switch (state) { case "running": case "wait-input": data.prolog.abort(); break; case "wait-next": data.prolog.stop(); } }); }, /** * Ask the associated Prolog engines for the next answer. * @param {Integer} chunk maximum number of answers to return in the * next chunk. */ next: function(chunk) { return this.each(function() { var elem = $(this); var data = elem.data('prologRunner'); data.prolog.next(chunk); elem.prologRunner('setState', "running"); }); }, /** * Detach the query from this runner. */ detach: function() { return this.each(function() { var elem = $(this); var data = elem.data('prologRunner'); data.prolog.detach({ query: data.query.query, state: data.prolog.state }); }); }, /** * Abort the associated Prolog engine. */ abort: function() { return this.each(function() { var elem = $(this); var data = elem.data('prologRunner'); data.prolog.abort(); }); }, /** * If the associated pengine is alive, send it an `abort`. Next, * remove the runner from its container. */ close: function() { if (this.length) { var parents = this.parent(); this.each(function() { var elem = $(this); var data = elem.data('prologRunner'); if (elem.prologRunner('alive')) { $(".prolog-editor").trigger('pengine-died', data.prolog.id); if (data.prolog.state != 'detached') { data.prolog.abort(); elem.prologRunner('setState', 'aborted'); } elem.prologRunner('stopOrAbort'); } }); this.remove(); parents.trigger('scroll-to-bottom', true); } return this; }, /** * Provide help on running a query */ help: function() { $(".swish-event-receiver").trigger("help", { file: "runner.html" }); }, /** * Toggle or set the iconic state of the runner. * @param {Boolean} [on] if `true`, make iconify, `false` expanded * and toggle if unspecified */ toggleIconic: function(on) { if (on == undefined) { this.toggleClass("iconic"); } else if (on) { this.addClass("iconic"); } else { this.removeClass("iconic"); } this.trigger('scroll-to-bottom', true); return this; }, /** * Populate the menu associated with the pengine icon. * @param {Object} [actions] associates labels with functions. */ populateActionMenu: function(actions) { var menu = this.find(".runner-title .btn-group.dropdown"); actions = $.extend({ "Re-run": function() { console.log("Re-Run ", this); } }, actions); form.widgets.populateMenu(menu, this, actions); return this; }, /** * Download query results as CSV. */ downloadCSV: function(options) { var data = this.data('prologRunner'); var query = termNoFullStop(data.query.query); prolog.downloadCSV(query, data.query.source, options); return this; }, /** * Save a permalink */ permalink: function() { var runner = this; var data = this.data('prologRunner'); if (data.permahash) { var href = config.http.locations.permalink + data.permahash; href = location.protocol + "//" + location.host + href; var profile = $("#login").login('get_profile', ["display_name", "avatar", "email", "identity" ]); var author = profile.display_name; function savePermalink() { this.append($.el.form({ class: "form-horizontal" }, form.fields.hidden("identity", profile.identity), profile.identity ? undefined : form.fields.hidden("avatar", profile.avatar), form.fields.link(href), form.fields.fileName(null, false), form.fields.title(), form.fields.description(), form.fields.tags([]), form.fields.author(author, profile.identity), form.fields.buttons({ label: "Save permalink", action: function(ev, as) { runner.prologRunner('save_permalink', as); return false; } }))); } form.showDialog({ title: "Save permalink", body: savePermalink }); } else { modal.alert("No permahash"); } return this; }, save_permalink: function(as) { var runner = this; var data = this.data('prologRunner'); var post = { data: data.permahash, type: "lnk", meta: as }; delete post.meta.link; $.ajax({ url: config.http.locations.web_storage, dataType: "json", contentType: "application/json", type: "POST", data: JSON.stringify(post), success: function(reply) { if (reply.error) { modal.alert(errorString("Could not save", reply)); } else { modal.feedback({ html: "Saved", owner: runner }); } }, error: function(jqXHR, textStatus, errorThrown) { if (jqXHR.status == 403) { modal.alert("Permission denied. Please try a different name"); } else { alert('Save failed: ' + textStatus); } } }); return this; }, /** * @param {String} state defines the new state of the pengine. * Known states are: * * - "idle" - Pengine is not yet created * - "running" - Pengine is running * - "wait-next" - Pengine produced a non-deterministic answer * - "wait-input" - Pengine waits for input * - "wait-debug" - Pengine waits for for debugger reply * - "true" - Pengine produced the last answer * - "false" - Pengine failed * - "error" - Pengine raised an error * - "stopped" - User selected *stop* after non-det answer * - "aborted" - User aborted execution * - "detached" - User detached the query from the browser * * The widget is brought to the new state by adding the state as a * class to all members of the class `show-state`, which currently * implies the pengines icon at the top-left and a _controller_ div * created by controllerDiv(). */ setState: function(state) { var data = this.data('prologRunner'); if (!data) return; if (data.prolog.state != state) { var stateful = this.find(".show-state"); var query = data.query; stateful.removeClass(data.prolog.state).addClass(state); data.prolog.state = state; if (!aliveState(state) && data.savedFocus) { $(data.savedFocus).focus(); data.savedFocus = null; } else if (state == "wait-input") { this.find("input").focus(); } if (state == "true" && query.success) query.success.call(this, data.prolog); if (!aliveState(state) && query.complete) query.complete.call(this, data.prolog); } var runners = RS(this); if (!aliveState(state)) { var elem = this; $(".prolog-editor").trigger('pengine-died', data.prolog.id); data.prolog.destroy(); setTimeout(function() { elem.trigger('scroll-to-bottom') }, 100); } else if (state == "wait-next" || state == "true") { var elem = this; setTimeout(function() { elem.trigger('scroll-to-bottom') }, 100); } else { this.trigger('scroll-to-bottom'); } return this; }, /** @returns {String} representing the current state of the * query execution. * @see {@link setState} */ getState: function() { var data = this.data('prologRunner'); return data.prolog ? data.prolog.state : "idle"; }, /** * @returns {Boolean} true if the related pengine is alive. That * means it has state `"running"`, `"wait-next"`, `"wait-input"` or * `"wait-debug"` */ alive: function() { return aliveState(this.prologRunner('getState')); }, /** * Handle ping data, updating the sparkline status */ ping: function(stats) { var data = this.data('prologRunner'); if (data && data.prolog && data.prolog.state == "running") { var spark = this.find(".sparklines"); var stacks = ["global", "local", "trail"]; var colors = ["red", "blue", "green"]; var names = ["Global ", "Local ", "Trail "]; var maxlength = 10; if (!data.stacks) data.stacks = { global: { usage: [] }, local: { usage: [] }, trail: { usage: [] } }; for (i = 0; i < stacks.length; i++) { var s = stacks[i]; var limit = stats.stacks[s].limit || stats.stacks.total.limit; var usage = stats.stacks[s].usage; var u = Math.log10((usage / limit) * 10000); function toBytes(limit, n) { var bytes = Math.round((Math.pow(10, n) / 10000) * limit); function numberWithCommas(x) { x = x.toString(); var pattern = /(-?\d+)(\d{3})/; while (pattern.test(x)) x = x.replace(pattern, "$1,$2"); return x; } return numberWithCommas(bytes); } data.stacks[s].limit = limit; if (data.stacks[s].usage.length >= maxlength) data.stacks[s].usage = data.stacks[s].usage.slice(1); data.stacks[s].usage.push(u); spark.sparkline(data.stacks[s].usage, { height: "2em", composite: i > 0, chartRangeMin: 0, chartRangeMax: 4, lineColor: colors[i], tooltipPrefix: names[i], tooltipSuffix: " bytes", tooltipChartTitle: i == 0 ? "Stack usage" : undefined, numberFormatter: function(n) { return toBytes(limit, n); } }); } } } }; // methods /******************************* * PRIVATE FUNCTIONS * *******************************/ function RS(from) { /* find runners from parts */ return $(from).closest(".prolog-runners"); } function addAnswer(runner, html) { var results = runner.find(".runner-results"); results.append(html); return this; } function aliveState(state) { switch (state) { case "running": case "detached": case "wait-next": case "wait-input": case "wait-debug": return true; default: return false; } } function answerTable(projection) { var tds = [{ class: "projection" }]; for (i = 0; i < projection.length; i++) tds.push($.el.th({ class: "pl-pvar" }, projection[i])); tds.push($.el.th({ class: "answer-nth" }, "")); var table = $.el.table({ class: "prolog-answers" }, $.el.tbody($.el.tr.apply(this, tds))); return table; } /******************************* * SCRIPTS IN NODES * *******************************/ var node_id = 1; function runScripts(elem) { var scripts = []; elem = $(elem); elem.find("script").each(function() { var type = this.getAttribute('type') || "text/javascript"; if (type == "text/javascript") scripts.push(this.textContent); }); if (scripts.length > 0) { var script = "(function(node){" + scripts.join("\n") + "})"; var node = new Node({ node: elem[0] }); try { eval(script)(node); } catch (e) { console.log("node=" + node); console.log("Script=" + script); alert(e); } } } function Node(options) { this.my_node = options.node; } Node.prototype.node = function() { return $(this.my_node); } /** * Provide a unique id for the node. This can be used as prefix to * avoid conflicts for `id` attributes. */ Node.prototype.unique_id = function() { if (!this.uid) this.uid = node_id++; return this.uid; } /******************************* * HANDLE PROLOG CALLBACKS * *******************************/ function breakpoints(runner) { var data = runner.data(pluginName); return $(runner).parents(".swish").swish('breakpoints', data.prolog.id); } function registerSources(pengine) { var runner = pengine.options.runner; var data = runner.data(pluginName); if (data.query.editor) $(data.query.editor).prologEditor('pengine', { add: pengine.id }); } function handleCreate() { var elem = this.pengine.options.runner; var data = elem.data(pluginName); if (data == undefined) { this.pengine.destroy(); /* element already gone */ } else { var options = $.extend({}, data.screen); var bps; var resvar = config.swish.residuals_var || "Residuals"; var hashvar = config.swish.permahash_var; var wfsresvar = config.swish.wfs_residual_program_var; if (hashvar) hashvar = ", " + hashvar; else hashvar = ""; if (wfsresvar) wfsresvar = ", " + wfsresvar; else wfsresvar = ""; registerSources(this.pengine); if ((bps = breakpoints(elem))) options.breakpoints = Pengine.stringify(bps); if (data.chunk) options.chunk = data.chunk; if (data.query.tabled) options.tabled = true; this.pengine.ask("'$swish wrapper'((\n" + termNoFullStop(data.query.query) + "\n), [" + resvar + hashvar + wfsresvar + "])", options); elem.prologRunner('setState', "running"); } } function handleSuccess() { var elem = this.pengine.options.runner; /* Handle the s(CASP) bindings. These are passed in reserved bindings * as escaped HTML holding a Prolog string. * TBD: Consider a clear way to pass real HTML around! */ function specialBindings(answer) { var vl = []; for(var i=0; i