summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorjohn.j.camilleri <john.j.camilleri@chalmers.se>2012-11-13 15:14:49 +0000
committerjohn.j.camilleri <john.j.camilleri@chalmers.se>2012-11-13 15:14:49 +0000
commit5c8c1f768fae242ba659d4b6a3696336042e0b1c (patch)
tree463f4007559a5b43cc3f789dd0e21b6a72b981fd /src
parent27e675910a88fec3d7f0cc0ac6020d86f1089fe7 (diff)
Add first demo of new syntax editor
As part of the GF cloud stuff, it can be accessed from http://cloud.grammaticalframework.org/syntax-editor/editor.html
Diffstat (limited to 'src')
-rw-r--r--src/www/index.html3
-rw-r--r--src/www/syntax-editor/editor.html43
-rw-r--r--src/www/syntax-editor/js/ast.js131
-rw-r--r--src/www/syntax-editor/js/editor.js216
-rw-r--r--src/www/syntax-editor/js/editor_menu.js153
-rw-r--r--src/www/syntax-editor/js/pgf_online.js80
-rw-r--r--src/www/syntax-editor/js/support.js329
-rw-r--r--src/www/syntax-editor/ui/style.css64
8 files changed, 1018 insertions, 1 deletions
diff --git a/src/www/index.html b/src/www/index.html
index df50c4dbe..c1e62bf45 100644
--- a/src/www/index.html
+++ b/src/www/index.html
@@ -13,6 +13,7 @@
<ul>
<li><a href="minibar/minibar.html">Minibar</a>
+ <li><a href="syntax-editor/editor.html">Syntax Editor (beta)</a>
<li><a href="TransQuiz/translation_quiz.html">Translation Quiz</a>
<li><a href="gfse/">GF online editor for simple multilingual grammars</a>
<li><a href="translator/">Simple Translation Tool</a>
@@ -33,4 +34,4 @@
<hr>
<a href="http://www.grammaticalframework.org/">www.grammaticalframework.org</a>
-<div class=modtime><small><a href="/version">GF version</a></small></div> \ No newline at end of file
+<div class=modtime><small><a href="/version">GF version</a></small></div>
diff --git a/src/www/syntax-editor/editor.html b/src/www/syntax-editor/editor.html
new file mode 100644
index 000000000..d641998ab
--- /dev/null
+++ b/src/www/syntax-editor/editor.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <link rel="author" href="http://www.grammaticalframework.org/~john/" title="John J. Camilleri">
+ <title>Syntax Editor</title>
+ <link rel="stylesheet" type="text/css" href="http://cloud.grammaticalframework.org/minibar/minibar.css" />
+ <link rel="stylesheet" type="text/css" href="ui/style.css" />
+</head>
+<body>
+ <h2>Syntax Editor</h2>
+ <div id="editor"></div>
+ <noscript>This page doesn't works unless JavaScript is enabled.</noscript>
+
+ <hr />
+ <small class="modtime">
+ John J. Camilleri, November 2012
+ </small>
+
+ <script type="text/javascript" src="js/support.js"></script>
+ <script type="text/javascript" src="js/pgf_online.js"></script>
+ <script type="text/javascript" src="js/ast.js"></script>
+ <script type="text/javascript" src="js/editor_menu.js"></script>
+ <script type="text/javascript" src="js/editor.js"></script>
+ <script type="text/javascript">
+ // Code taken from minibar_online.js
+ var server_options = {
+ //grammars_url: "http://www.grammaticalframework.org/grammars/",
+ //grammars_url: "http://localhost:41296/grammars/",
+ }
+ var editor_options = {
+ target: "editor"
+ }
+ if(/^\?\/tmp\//.test(location.search)) {
+ var args=decodeURIComponent(location.search.substr(1)).split(" ")
+ if(args[0]) server_options.grammars_url=args[0];
+ }
+ var server = pgf_online(server_options);
+ var editor = new Editor(server, editor_options);
+ </script>
+
+</body>
+</html>
diff --git a/src/www/syntax-editor/js/ast.js b/src/www/syntax-editor/js/ast.js
new file mode 100644
index 000000000..de9e9d3eb
--- /dev/null
+++ b/src/www/syntax-editor/js/ast.js
@@ -0,0 +1,131 @@
+/* --- Tree representation -------------------------------------------------- */
+function Tree(value) {
+
+ // Create node as JS object
+ var createNode = function(value, children) {
+ var node = {
+ value: value,
+ children: []
+ };
+ if (children != undefined)
+ for (c in children)
+ node.children.push( createNode(children[c],[]) );
+ return node;
+ }
+
+ this.root = createNode(value, []);
+
+ // add value as child of id
+ this.add = function(id, value, children) {
+ var x = this.find(id);
+ x.children.push( createNode(value, children) );
+ }
+
+ // id should be a list of child indices [0,1,0]
+ // or a string separated by commas "0,1,0"
+ this.find = function(_id) {
+ var id = undefined
+ switch (typeof _id) {
+ case "number": id = [_id]; break;
+ case "string": id = _id.split(","); break;
+ case "object": id = _id.get().slice(); break; // clone NodeID array
+ }
+ var node = this.root;
+ if (id[0] == 0) id.shift();
+ while (id.length>0 && node.children.length>0) {
+ node = node.children[id.shift()];
+ }
+ if (id.length>0)
+ return undefined;
+ return node;
+ }
+}
+
+/* --- ID for a node in a tree ---------------------------------------------- */
+function NodeID(x) {
+ this.id = new Array();
+ this.id.push(0);
+
+ // Initialize from input
+ if (x) {
+ switch (typeof x) {
+ case "number": this.id = [x]; break;
+ case "string": this.id = map(function(s){return parseInt(s)}, x.split(",")); break;
+ case "object": this.id = x.get().slice(); break; // another NodeID
+ }
+ }
+
+ // get id
+ this.get = function() {
+ return this.id;
+ }
+
+ // Add child node to id
+ this.add = function(x) {
+ this.id.push(parseInt(x));
+ return this.id;
+ }
+
+ // compare with other id
+ this.equals = function(other) {
+ return JSON.stringify(this.id)==JSON.stringify(other.id);
+ }
+
+}
+
+/* --- Abstract Syntax Tree (with state)------------------------------------- */
+function AST(fun, cat) {
+
+ function Node(fun, cat) {
+ this.fun = fun;
+ this.cat = cat;
+ }
+
+ this.tree = new Tree(new Node(fun, cat));
+ this.current = new NodeID(); // current id in tree
+
+ this.getFun = function() {
+ return this.tree.find(this.current).value.fun;
+ }
+ this.setFun = function(f) {
+ this.tree.find(this.current).value.fun = f;
+ }
+ this.getCat = function() {
+ return this.tree.find(this.current).value.cat;
+ }
+ this.setCat = function(c) {
+ this.tree.find(this.current).value.cat = c;
+ }
+
+ this.add = function(fun, cat) {
+ this.tree.add(this.current, new Node(fun,cat));
+ }
+
+ // Clear children of current node
+ this.removeChildren = function() {
+ this.tree.find(this.current).children = [];
+ }
+
+ // Move current id to child number i
+ this.toChild = function(i) {
+ this.current.add(i);
+ }
+
+ // Return tree as string
+ this.toString = function() {
+ var s = "";
+ function visit(node) {
+ s += node.value.fun ? node.value.fun : "?" ;
+ if (node.children.length == 0)
+ return;
+ for (i in node.children) {
+ s += "(";
+ visit(node.children[i]);
+ s += ")";
+ }
+ }
+ visit(this.tree.root);
+ return s;
+ }
+}
+
diff --git a/src/www/syntax-editor/js/editor.js b/src/www/syntax-editor/js/editor.js
new file mode 100644
index 000000000..21f801db0
--- /dev/null
+++ b/src/www/syntax-editor/js/editor.js
@@ -0,0 +1,216 @@
+
+/* --- Some enhancements --------------------------------------------------- */
+
+// http://www.xenoveritas.org/blog/xeno/the-correct-way-to-clone-javascript-arrays
+// Array.prototype.clone = function(){
+// return this.slice(0);
+// }
+
+/* --- Main Editor object --------------------------------------------------- */
+function Editor(server,opts) {
+ var t = this;
+ /* --- Configuration ---------------------------------------------------- */
+
+ // default values for options:
+ this.options={
+ target: "editor"
+ }
+
+ // Apply supplied options
+ if(opts) for(var o in opts) this.options[o]=opts[o];
+
+ /* --- Creating UI components ------------------------------------------- */
+ var main = document.getElementById(this.options.target);
+ this.ui = {
+ menubar: div_class("menu"),
+ tree: div_id("tree"),
+ refinements: div_id("refinements"),
+ lin: div_id("linearisations")
+ };
+ with(this.ui) {
+ appendChildren(main, [menubar, tree, refinements, lin]);
+ }
+
+ /* --- Client state initialisation -------------------------------------- */
+ this.server = server;
+ this.ast = null;
+ this.grammar = null;
+ this.languages = [];
+ this.local = {}; // local settings which may override grammar
+
+ /* --- Main program, this gets things going ----------------------------- */
+ this.menu = new EditorMenu(this);
+
+}
+
+/* --- API for getting and setting state ------------------------------------ */
+
+Editor.prototype.get_ast=function() {
+ return this.ast.toString();
+}
+
+Editor.prototype.get_startcat=function() {
+ return this.local.startcat || this.grammar.startcat;
+}
+
+/* --- These get called from EditorMenu, or some custom code */
+
+Editor.prototype.change_grammar=function(grammar_info) {
+ with(this) {
+ grammar = grammar_info;
+ local.startcat = null;
+ start_fresh();
+ }
+}
+
+Editor.prototype.change_startcat=function(startcat) {
+ with(this) {
+ local.startcat = startcat;
+ start_fresh();
+ }
+}
+
+// Called after changing grammar or startcat
+Editor.prototype.start_fresh=function () {
+ with(this) {
+ ast = new AST(null, get_startcat());
+ redraw_tree();
+ update_current_node();
+ get_refinements();
+ clear(ui.lin);
+ }
+}
+
+/* --- Functions for handling tree manipulation ----------------------------- */
+
+Editor.prototype.get_refinements=function(cat) {
+ with (this) {
+ if (cat == undefined)
+ cat = ast.getCat();
+ var args = {
+ id: cat,
+ format: "json"
+ };
+ var cont = function(data){
+ clear(refinements);
+ for (pi in data.producers) {
+ var opt = span_class("refinement", text(data.producers[pi]));
+ opt.onclick = function(){ select_refinement(this.innerText) };
+ refinements.appendChild(opt);
+ }
+ };
+ var err = function(data){
+ clear(refinements);
+ alert("no refinements");
+ };
+ server.browse(args,cont,err);
+ }
+}
+
+Editor.prototype.select_refinement=function(fun) {
+ with (this) {
+ ast.removeChildren();
+ ast.setFun(fun);
+ var args = {
+ id: fun,
+ format: "json"
+ };
+ var err = function(data){
+ clear(refinements);
+ alert("no refinements");
+ };
+ server.browse(args, bind(complete_refinement,this), err);
+ }
+}
+
+Editor.prototype.complete_refinement=function(data) {
+ if (!data) return;
+
+ with (this) {
+ // Parse out function arguments
+ var def = data.def;
+ def = def.substr(def.lastIndexOf(":")+1);
+ var fun_args = map(function(s){return s.trim()}, def.split("->"))
+ fun_args = fun_args.slice(0,-1);
+
+ if (fun_args.length > 0) {
+ // Add placeholders
+ for (ci in fun_args) {
+ ast.add(null, fun_args[ci]);
+ }
+
+ // Select next hole & get its refinements
+ ast.toChild(0);
+ update_current_node();
+ get_refinements();
+ }
+ redraw_tree();
+ update_linearisation();
+ }
+}
+
+Editor.prototype.update_current_node=function(newID) {
+ with(this) {
+ if (newID)
+ ast.current = new NodeID(newID);
+ redraw_tree();
+ get_refinements();
+ }
+}
+
+Editor.prototype.redraw_tree=function() {
+ var t = this;
+ var elem = node; // function from support.js
+ function visit(container, id, node) {
+ var container2 = empty_class("div", "node");
+ var label = ((node.value.fun) ? node.value.fun : "?") + " : " + node.value.cat;
+ var current = id.equals(t.ast.current);
+ var element = elem("a", {class:(current?"current":"")}, [text(label)]);
+ element.onclick = function() {
+ t.update_current_node(id);
+ }
+ container2.appendChild( element );
+
+ for (i in node.children) {
+ var newid = new NodeID(id);
+ newid.add(parseInt(i));
+ visit(container2, newid, node.children[i]);
+ }
+
+ container.appendChild(container2);
+ }
+ with(this) {
+ clear(ui.tree);
+ visit(ui.tree, new NodeID(), ast.tree.root);
+ }
+}
+
+Editor.prototype.update_linearisation=function(){
+
+ function langpart(conc,abs) { // langpart("FoodsEng","Foods") == "Eng"
+ return hasPrefix(conc,abs) ? conc.substr(abs.length) : conc;
+ }
+
+ var t = this;
+ with (this) {
+ var args = {
+ tree: ast.toString()
+ };
+ server.linearize(args, function(data){
+ clear(t.ui.lin);
+ for (i in data) {
+ var lang = data[i].to;
+ var langname = langpart(lang, t.grammar.name);
+ if (t.languages.length < 1 || elem(lang, t.languages)) {
+ var div_lang = empty("div");
+ div_lang.appendChild(span_class("lang", text(langname)));
+ div_lang.appendChild(
+ span_class("lin", [text(data[i].text)])
+ );
+ t.ui.lin.appendChild(div_lang);
+ }
+ }
+ });
+ }
+}
+
diff --git a/src/www/syntax-editor/js/editor_menu.js b/src/www/syntax-editor/js/editor_menu.js
new file mode 100644
index 000000000..874859ae7
--- /dev/null
+++ b/src/www/syntax-editor/js/editor_menu.js
@@ -0,0 +1,153 @@
+/* --- Editor Menu object --------------------------------------------------- */
+function EditorMenu(editor,opts) {
+ var t = this;
+ /* --- Configuration ---------------------------------------------------- */
+
+ // default values for options:
+ this.options={
+ target: "editor"
+ }
+
+ // Apply supplied options
+ if(opts) for(var o in opts) this.options[o]=opts[o];
+
+ /* --- Creating UI components ------------------------------------------- */
+ this.container = editor.ui.menubar;
+ this.ui = {
+ startcat_menu: empty("select"),
+ to_menu: empty_id("select","to_menu")
+ };
+ with(this.ui) {
+ appendChildren(this.container, [text(" Startcat: "),startcat_menu]);
+ appendChildren(this.container, [text(" To: "), to_menu]);
+ startcat_menu.onchange=bind(this.change_startcat,this);
+ to_menu.onchange=bind(this.change_language,this);
+ }
+
+ /* --- Client state initialisation -------------------------------------- */
+ this.editor = editor;
+ this.server = editor.server;
+
+ /* --- Main program, this gets things going ----------------------------- */
+ with(this) {
+ server.get_grammarlists(bind(show_grammarlist,this));
+ }
+}
+
+// Copied from minibar.js
+EditorMenu.prototype.show_grammarlist=function(dir,grammar_names,dir_count) {
+ var t=this;
+ var first_time= !t.grammar_menu
+ if(first_time) {
+ t.grammar_menu=empty_id("select","grammar_menu");
+ t.grammars=[];
+ t.grammar_dirs=[];
+ }
+ with(t) {
+ grammar_dirs.push(dir);
+ grammars=grammars.concat(grammar_names.map(function(g){return dir+g}))
+ function glabel(g) {
+ return hasPrefix(dir,"/tmp/gfse.") ? "gfse: "+g : g
+ }
+ function opt(g) { return option(glabel(g),dir+g); }
+ appendChildren(grammar_menu,map(opt,grammar_names));
+ function pick() {
+ var grammar_url=grammar_menu.value
+ if(window.localStorage)
+ localStorage["gf.minibar.last_grammar"]=grammar_url;
+ t.select_grammar(grammar_url);
+ }
+ function pick_first_grammar() {
+ if(t.timeout) clearTimeout(t.timeout),t.timeout=null;
+ if(t.grammar_menu.length>1 && !t.grammar_menu.parentElement) {
+ t.grammar_menu.onchange=pick;
+// insertFirst(t.menubar,button("i",bind(t.show_grammarinfo,t)))
+ insertFirst(t.container,t.grammar_menu);
+ insertFirst(t.container,text("Grammar: "));
+ }
+ var grammar0=t.options.initial_grammar
+ if(!grammar0 && window.localStorage) {
+ var last_grammar=localStorage["gf.minibar.last_grammar"];
+ if(last_grammar && elem(last_grammar,t.grammars))
+ grammar0=last_grammar;
+ }
+ if(!grammar0) grammar0=t.grammars[0];
+ t.grammar_menu.value=grammar0;
+ t.select_grammar(grammar0);
+ }
+ // Wait at most 1.5s before showing the grammar menu.
+ if(first_time) t.timeout=setTimeout(pick_first_grammar,1500);
+ if(t.grammar_dirs.length>=dir_count) pick_first_grammar();
+ }
+}
+
+// Copied from minibar.js
+EditorMenu.prototype.select_grammar=function(grammar_url) {
+ var t=this;
+ //debug("select_grammar ");
+ t.server.switch_to_other_grammar(grammar_url, function() {
+ t.server.grammar_info(function(grammar){
+ t.update_language_menu(t.ui.to_menu, grammar);
+ t.update_startcat_menu(grammar);
+
+ // Call in main Editor object
+ t.editor.change_grammar(grammar);
+ });
+ });
+}
+
+// Copied from minibar_input.js
+EditorMenu.prototype.update_startcat_menu=function(grammar) {
+ var menu=this.ui.startcat_menu;
+ menu.innerHTML="";
+ var cats=grammar.categories;
+ for(var cat in cats) menu.appendChild(option(cats[cat],cats[cat]))
+// var startcat=this.local.get("startcat") || grammar.startcat;
+ var startcat= grammar.startcat;
+ if(startcat) menu.value=startcat;
+ else {
+ insertFirst(menu,option("Default",""));
+ menu.value="";
+ }
+}
+
+//
+EditorMenu.prototype.change_startcat=function () {
+ var new_startcat = this.ui.startcat_menu.value;
+ this.editor.change_startcat(new_startcat);
+}
+
+//
+EditorMenu.prototype.change_language=function () {
+ if (this.ui.to_menu.value == "All")
+ this.editor.languages = new Array();
+ else
+ this.editor.languages = new Array(this.ui.to_menu.value);
+ this.editor.update_linearisation();
+}
+
+// Copied from minibar_support.js
+EditorMenu.prototype.update_language_menu=function(menu,grammar) {
+
+ function langpart(conc,abs) { // langpart("FoodsEng","Foods") == "Eng"
+ return hasPrefix(conc,abs) ? conc.substr(abs.length) : conc;
+ }
+
+ // Replace the options in the menu with the languages in the grammar
+ var lang=grammar.languages;
+ menu.innerHTML="";
+
+ for(var i=0; i<lang.length; i++) {
+ var ln=lang[i].name;
+ if(!hasPrefix(ln,"Disamb")) {
+ var lp=langpart(ln,grammar.name);
+ menu.appendChild(option(lp,ln));
+ }
+ }
+
+ insertFirst(menu,option("All","All"));
+ menu.value="All";
+}
+
+
+
diff --git a/src/www/syntax-editor/js/pgf_online.js b/src/www/syntax-editor/js/pgf_online.js
new file mode 100644
index 000000000..6fe23b13e
--- /dev/null
+++ b/src/www/syntax-editor/js/pgf_online.js
@@ -0,0 +1,80 @@
+
+/* --- Grammar access object ------------------------------------------------ */
+
+function pgf_online(options) {
+ var server = {
+ // State variables (private):
+ grammars_url: "/grammars/",
+ other_grammars_urls: [],
+ grammar_list: null,
+ current_grammar_url: null,
+
+ // Methods:
+ switch_grammar: function(grammar_url,cont) {
+ this.current_grammar_url=this.grammars_url+grammar_url;
+ if(cont) cont();
+ },
+ add_grammars_url: function(grammars_url,cont) {
+ this.other_grammars_urls.push(grammars_url);
+ if(cont) cont();
+ },
+ switch_to_other_grammar: function(grammar_url,cont) {
+ this.current_grammar_url=grammar_url;
+ if(cont) cont();
+ },
+ get_grammarlist: function(cont,err) {
+ if(this.grammar_list) cont(this.grammar_list)
+ else http_get_json(this.grammars_url+"grammars.cgi",cont,err);
+ },
+ get_grammarlists: function(cont,err) { // May call cont several times!
+ var ds=this.other_grammars_urls;
+ var n=1+ds.length;
+ function pair(dir) {
+ return function(grammar_list){cont(dir,grammar_list,n)}
+ }
+ function ignore_error(err) { console.log(err) }
+ this.get_grammarlist(pair(this.grammars_url),err)
+ for(var i in ds)
+ http_get_json(ds[i]+"grammars.cgi",pair(ds[i]),ignore_error);
+ },
+ pgf_call: function(cmd,args,cont,err) {
+ var url=this.current_grammar_url+"?command="+cmd+encodeArgs(args)
+ http_get_json(url,cont,err);
+ },
+
+ get_languages: function(cont,err) {
+ this.pgf_call("grammar",{},cont,err);
+ },
+ grammar_info: function(cont,err) {
+ this.pgf_call("grammar",{},cont,err);
+ },
+
+ get_random: function(args,cont,err) { // cat, limit
+ args.random=Math.random(); // side effect!!
+ this.pgf_call("random",args,cont,err);
+ },
+ linearize: function(args,cont,err) { // tree, to
+ this.pgf_call("linearize",args,cont,err);
+ },
+ complete: function(args,cont,err) { // from, input, cat, limit
+ this.pgf_call("complete",args,cont,err);
+ },
+ parse: function(args,cont,err) { // from, input, cat
+ this.pgf_call("parse",args,cont,err);
+ },
+ translate: function(args,cont,err) { // from, input, cat, to
+ this.pgf_call("translate",args,cont,err);
+ },
+ translategroup: function(args,cont,err) { // from, input, cat, to
+ this.pgf_call("translategroup",args,cont,err);
+ },
+ browse: function(args,cont,err) { // id, format
+ if(!args.format) args.format="json"; // sife effect!!
+ this.pgf_call("browse",args,cont,err);
+ }
+ };
+ for(var o in options) server[o]=options[o];
+ if(server.grammar_list && server.grammar_list.length>0)
+ server.switch_grammar(server.grammar_list[0]);
+ return server;
+}
diff --git a/src/www/syntax-editor/js/support.js b/src/www/syntax-editor/js/support.js
new file mode 100644
index 000000000..e82f4fc9c
--- /dev/null
+++ b/src/www/syntax-editor/js/support.js
@@ -0,0 +1,329 @@
+/* --- Accessing document elements ------------------------------------------ */
+
+function element(id) {
+ return document.getElementById(id);
+}
+
+/* --- JavaScript tricks ---------------------------------------------------- */
+
+// To be able to use object methods that refer to "this" as callbacks
+// See section 3.3 of https://github.com/spencertipping/js-in-ten-minutes/raw/master/js-in-ten-minutes.pdf
+function bind(f, this_value) {
+ return function () {return f.apply (this_value, arguments)};
+};
+
+// Implement Array.isArray for older browsers that lack it.
+// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray
+if(!Array.isArray) {
+ Array.isArray = function (arg) {
+ return Object.prototype.toString.call(arg) == '[object Array]';
+ };
+}
+
+/* --- JSONP ---------------------------------------------------------------- */
+
+// Inspired by the function jsonp from
+// http://www.west-wind.com/Weblog/posts/107136.aspx
+// See also http://niryariv.wordpress.com/2009/05/05/jsonp-quickly/
+// http://en.wikipedia.org/wiki/JSONP
+function jsonp(url,callback)
+{
+ if (url.indexOf("?") > -1)
+ url += "&jsonp="
+ else
+ url += "?jsonp="
+ url += callback;
+ //url += "&" + new Date().getTime().toString(); // prevent caching
+
+ var script = empty("script");
+ script.setAttribute("src",url);
+ script.setAttribute("type","text/javascript");
+ document.body.appendChild(script);
+}
+
+var json = {next:0};
+
+// Like jsonp, but instead of passing the name of the callback function, you
+// pass the callback function directly, making it possible to use anonymous
+// functions.
+function jsonpf(url,callback,errorcallback)
+{
+ var name="callback"+(json.next++);
+ json[name]=function(x) { delete json[name]; callback(x); }
+ jsonp(url,"json."+name);
+}
+
+/* --- AJAX ----------------------------------------------------------------- */
+
+function GetXmlHttpObject(handler)
+{
+ var objXMLHttp=null
+ if (window.XMLHttpRequest)
+ {
+ // See http://www.w3.org/TR/XMLHttpRequest/
+ // https://developer.mozilla.org/en/xmlhttprequest
+ objXMLHttp=new XMLHttpRequest()
+ }
+ else if (window.ActiveXObject)
+ {
+ objXMLHttp=new ActiveXObject("Microsoft.XMLHTTP")
+ }
+ return objXMLHttp
+}
+
+function ajax_http(method,url,body,callback,errorcallback) {
+ var http=GetXmlHttpObject()
+ if (!http) {
+ var errortext="Browser does not support HTTP Request";
+ if(errorcallback) errorcallback(errortext,500)
+ else alert(errortext)
+ }
+ else {
+ function statechange() {
+ if (http.readyState==4 || http.readyState=="complete") {
+ if(http.status<300) callback(http.responseText,http.status);
+ else if(errorcallback)
+ errorcallback(http.responseText,http.status,
+ http.getResponseHeader("Content-Type"));
+ else alert("Request for "+url+" failed: "
+ +http.status+" "+http.statusText);
+ }
+ }
+ http.onreadystatechange=statechange;
+ http.open(method,url,true)
+ http.send(body)
+ }
+ return http
+}
+
+function ajax_http_get(url,callback,errorcallback) {
+ ajax_http("GET",url,null,callback,errorcallback)
+}
+
+function ajax_http_post(url,formdata,callback,errorcallback) {
+ ajax_http("POST",url,formdata,callback,errorcallback)
+ // See https://developer.mozilla.org/En/XMLHttpRequest/Using_XMLHttpRequest#Using_FormData_objects
+}
+
+// JSON via AJAX
+function ajax_http_get_json(url,cont,errorcallback) {
+ ajax_http_get(url,function(txt){cont(eval("("+txt+")"));}, errorcallback);
+}
+
+function sameOrigin(url) {
+ var a=empty("a");
+ a.href=url; // converts to an absolute URL
+ return hasPrefix(a.href,location.protocol+"//"+location.host+"/");
+}
+
+// Use AJAX when possible, fallback to JSONP
+function http_get_json(url,cont,errorcallback) {
+ if(sameOrigin(url)) ajax_http_get_json(url,cont,errorcallback);
+ else jsonpf(url,cont,errorcallback);
+}
+
+/* --- URL construction ----------------------------------------------------- */
+
+function encodeArgs(args) {
+ var q=""
+ for(var arg in args)
+ if(args[arg]!=undefined)
+ q+="&"+arg+"="+encodeURIComponent(args[arg]);
+ return q;
+}
+
+/* --- HTML construction ---------------------------------------------------- */
+function text(s) { return document.createTextNode(s); }
+
+function node(tag,as,ds) {
+ var n=document.createElement(tag);
+ for(var a in as) n.setAttribute(a,as[a]);
+ if(ds) for(var i in ds) n.appendChild(ds[i]);
+ return n;
+}
+
+function empty(tag,name,value) {
+ var el=node(tag,{},[])
+ if(name && value) el.setAttribute(name,value);
+ return el;
+}
+
+function empty_id(tag,id) { return empty(tag,"id",id); }
+function empty_class(tag,cls) { return empty(tag,"class",cls); }
+
+function div_id(id,cs) { return node("div",{id:id},cs); }
+function span_id(id) { return empty_id("span",id); }
+
+function wrap(tag,contents) {
+ return node(tag,{},Array.isArray(contents) ? contents : [contents]);
+}
+
+function wrap_class(tag,cls,contents) {
+ return node(tag,{"class":cls},
+ contents ? Array.isArray(contents) ?
+ contents : [contents] : [])
+}
+
+function span_class(cls,contents) { return wrap_class("span",cls,contents); }
+function div_class(cls,contents) { return wrap_class("div",cls,contents); }
+
+function p(contents) { return wrap("p",contents); }
+function dt(contents) { return wrap("dt",contents); }
+function dd(contents) { return wrap("dd",contents); }
+function li(contents) { return wrap("li",contents); }
+
+function th(contents) { return wrap("th",contents); }
+function td(contents) { return wrap("td",contents); }
+
+function tr(cells) { return wrap("tr",cells); }
+
+function button(label,action,key) {
+ var el=node("input",{"type":"button","value":label},[]);
+ if(typeof action=="string") el.setAttribute("onclick",action);
+ else el.onclick=action;
+ if(key) el.setAttribute("accesskey",key);
+ return el;
+}
+
+function option(label,value) {
+ return node("option",{"value":value},[text(label)]);
+}
+
+function hidden(name,value) {
+ return node("input",{type:"hidden",name:name,value:value},[])
+}
+
+function tda(cs) { return node("td",{},cs); }
+
+function img(src) { return empty("img","src",src); }
+
+/* --- Document modification ------------------------------------------------ */
+
+function clear(el) { replaceInnerHTML(el,""); }
+function replaceInnerHTML(el,html) { if(el) el.innerHTML=html; }
+function replaceChildren(el,newchild) { clear(el); el.appendChild(newchild); }
+
+function appendChildren(el,ds) {
+ for(var i in ds) el.appendChild(ds[i]);
+ return el;
+}
+
+function insertFirst(parent,child) {
+ parent.insertBefore(child,parent.firstChild);
+}
+
+function insertBefore(el,ref) { ref.parentNode.insertBefore(el,ref); }
+
+function insertAfter(el,ref) {
+ ref.parentNode.insertBefore(el,ref.nextSibling);
+}
+
+/* --- Debug ---------------------------------------------------------------- */
+
+function debug(s) {
+ var d=element("debug");
+ if(d) d.appendChild(text(s+"\n"))
+}
+
+function show_props(obj, objName) {
+ var result = "";
+ for (var i in obj) {
+ result += objName + "." + i + " = " + obj[i] + "<br>";
+ }
+ return result;
+}
+
+function field_names(obj) {
+ var result = "";
+ for (var i in obj) {
+ result += " " + i;
+ }
+ return result;
+}
+
+/* --- Data manipulation ---------------------------------------------------- */
+function swap(a,i,j) { // Note: this doesn't work on strings.
+ var tmp=a[i];
+ a[i]=a[j];
+ a[j]=tmp;
+ return a;
+}
+
+function sort(a) {
+// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array/sort
+ return a.sort();
+ /* // Note: this doesn't work on strings.
+ for(var i=0;i<a.length-1;i++) {
+ var min=i;
+ for(var j=i+1;j<a.length;j++)
+ if(a[j]<a[min]) min=j;
+ if(min!=i) swap(a,i,min);
+ }
+ return a;
+ */
+}
+
+function filter(p,xs) {
+ var ys=[];
+ for(var i=0;i<xs.length;i++)
+ if(p(xs[i])) ys[ys.length]=xs[i];
+ return ys;
+}
+
+function implode(cs) { // array of strings to string
+ /*
+ var s="";
+ for(var i=0;i<cs.length;i++)
+ s+=cs[i];
+ return s;
+ */
+ return cs.join("");
+}
+
+function hasPrefix(s,pre) { return s.substr(0,pre.length)==pre; }
+
+function commonPrefix(s1,s2) {
+ for(var i=0;i<s1.length && i<s2.length && s1[i]==s2[i];i++);
+ return s1.substr(0,i);
+}
+
+/*
+function all(p,xs) {
+ for(var i=0;i<xs.length;i++)
+ if(!p(xs[i])) return false;
+ return true;
+}
+*/
+
+function map(f,xs) {
+ var ys=[];
+ for(var i=0;i<xs.length;i++) ys[i]=f(xs[i]);
+ return ys;
+}
+
+// map in continuation passing style
+function mapc(f,xs,cont) { mapc_from(f,xs,0,[],cont); }
+
+function mapc_from(f,xs,i,ys,cont) {
+ if(i<xs.length)
+ f(xs[i],function(y){ys[i]=y;mapc_from(f,xs,i+1,ys,cont)});
+ else
+ cont(ys);
+}
+
+function overlaps(as,bs) {
+ for(var i=0;i<as.length;i++)
+ if(elem(as[i],bs)) return true;
+ return false;
+}
+
+function elem(a,as) {
+ for(var i=0;i<as.length;i++)
+ if(a==as[i]) return true;
+ return false;
+}
+
+function shuffle(a) {
+ for(i=0;i<a.length;i++) swap(a,i,Math.floor(Math.random()*a.length))
+ return a;
+}
diff --git a/src/www/syntax-editor/ui/style.css b/src/www/syntax-editor/ui/style.css
new file mode 100644
index 000000000..f4f29d040
--- /dev/null
+++ b/src/www/syntax-editor/ui/style.css
@@ -0,0 +1,64 @@
+body {
+ background: #ccc url("http://cloud.grammaticalframework.org/minibar/brushed-metal.png");
+}
+
+#tree
+{
+ white-space:pre;
+ font-family: monospace;
+ background: rgba(238, 238, 238, 0.6);
+ padding:0.5em;
+ margin:0.5em 0;
+ }
+
+#tree .node
+{
+ margin: 0.4em 0 0.4em 1.5em;
+ }
+
+#tree .node a
+{
+ cursor: pointer;
+ }
+#tree .node a:hover
+{
+ text-decoration: underline;
+ }
+#tree .node a.current
+{
+ font-weight: bold;
+ }
+
+#linearisations
+{
+ background: rgba(170, 170, 170, 0.5);
+ padding:0.5em;
+ margin:0.5em 0;
+ }
+#linearisations div
+{
+ padding:0.2em;
+ }
+#linearisations .lang
+{
+ display: inline-block;
+ margin-right: 0.5em;
+ width: 3em;
+ font-weight: bold;
+ text-align: center;
+ }
+#linearisations .lin
+{
+ }
+
+.refinement
+{
+ margin: 0 0.1em;
+ display: inline-block;
+ cursor: pointer;
+ border: 1px solid;
+ padding: 0.2em;
+ font: 0.9em sans-serif;
+ background: white;
+ }
+