diff options
| author | hallgren <hallgren@chalmers.se> | 2012-10-04 15:21:30 +0000 |
|---|---|---|
| committer | hallgren <hallgren@chalmers.se> | 2012-10-04 15:21:30 +0000 |
| commit | 1eef49ac936947e34af470e339e00056fce50b87 (patch) | |
| tree | ebb944e2d7f56ed937c7abd54332498ffd756492 /src | |
| parent | ec9dc23f466499fa4c77f14553b2cb1d03ce0e82 (diff) | |
gfse: various code improvements and prelimiary support for public grammars
Diffstat (limited to 'src')
| -rw-r--r-- | src/www/gfse/cloud2.js | 83 | ||||
| -rw-r--r-- | src/www/gfse/editor.css | 6 | ||||
| -rw-r--r-- | src/www/gfse/editor.js | 240 | ||||
| -rw-r--r-- | src/www/gfse/index.html | 4 | ||||
| -rw-r--r-- | src/www/translator/about.html | 3 |
5 files changed, 252 insertions, 84 deletions
diff --git a/src/www/gfse/cloud2.js b/src/www/gfse/cloud2.js index 02af9d91a..b04be451a 100644 --- a/src/www/gfse/cloud2.js +++ b/src/www/gfse/cloud2.js @@ -74,7 +74,15 @@ function upload_grammars(gs,cont) { with_dir(upload2) } -// Upload the grammar to store it in the cloud +function assign_unique_name(g,unique_id) { + if(!g.unique_name) { + g.unique_name=unique_id+"-"+g.index; + save_grammar(g) + } + return g +} + +// Upload all grammars to the cloud function upload_json(cont) { function upload2(dir,unique_id) { function upload3(resptext,status) { @@ -105,10 +113,7 @@ function upload_json(cont) { for(var i=0;i<local.count;i++) { var g=local.get(i,null); if(g) { - if(!g.unique_name) { - g.unique_name=unique_id+"-"+i; - save_grammar(g) - } + g=assign_unique_name(g,unique_id) //form.append(g.unique_name+".json",JSON.stringify(g)); form[encodeURIComponent(g.unique_name+".json")]=JSON.stringify(g) } @@ -119,16 +124,49 @@ function upload_json(cont) { with_dir(upload2); } +function remove_public(name,cont,err) { + gfcloud_public_post("rm",{file:name},cont,err) +} + +// Publish a single grammar +function publish_json(g,cont) { + function publish2(dir,unique_id) { + var oldname=g.publishedAs + + function publish3(resptext,status) { + console.log("publish3") + if(oldname && oldname!=g.basename) { + console.log("old name="+oldname) + var name=oldname+"-"+g.unique_name+".json" + remove_public(name,cont,cont) + } + else cont() + } + g.publishedAs=g.basename; + save_grammar(g); + g=assign_unique_name(g,unique_id) + var name=g.basename+"-"+g.unique_name + var ix=g.index; + delete g.publishedAs + delete g.unique_name + delete g.index + var form={} + form[encodeURIComponent(name+".json")]=JSON.stringify(g) + g=reget_grammar(ix) + gfcloud_public_post("upload",form,publish3,cont) + } + with_dir(publish2); +} + function download_json() { var dir=local.get("dir"); - var index=grammar_index(); var downloading=0; function get_list(ok,err) { gfcloud("ls",{},ok,err) } function get_file(file,ok,err) { downloading++; - gfcloud("download",{file:encodeURIComponent(file)},ok,err); + gfcloud("download",{file:file},ok,err); } function file_failed(errormsg,status) { @@ -139,8 +177,8 @@ function download_json() { downloading--; var newg=JSON.parse(grammar); debug("Downloaded "+newg.unique_name) - var i=index[newg.unique_name]; - if(i!=undefined) merge_grammar(i,newg) + var i=my_grammar(newg.unique_name+".json"); + if(i!=null) merge_grammar(i,newg) else { debug("New") newg.index=null; @@ -176,7 +214,23 @@ function link_directories(newdir,cont) { /* -------------------------------------------------------------------------- */ -// Request GF cloud service +var public_dir="/tmp/public" + +// Request GF cloud service in the public directory (using GET) +function gfcloud_public_json(cmd,args,cont,err) { + var enc=encodeURIComponent; + var url="/cloud?dir="+public_dir+"&command="+enc(cmd)+encodeArgs(args) + http_get_json(url,cont,err) +} + +// Request GF cloud service in the public directory (using POST) +function gfcloud_public_post(cmd,args,cont,err) { + var enc=encodeURIComponent; + var req="dir="+public_dir+"&command="+enc(cmd)+encodeArgs(args) + ajax_http_post("/cloud",req,cont,err) +} + +// Request GF cloud service (using GET, for idempotent requests) function gfcloud(cmd,args,cont,err) { with_dir(function(dir) { var enc=encodeURIComponent; @@ -185,6 +239,15 @@ function gfcloud(cmd,args,cont,err) { }) } +// Reqest GF cloud service (using POST, for state changing requests) +function gfcloud_post(cmd,args,cont,err) { + with_dir(function(dir) { + var enc=encodeURIComponent; + var req="dir="+enc(dir)+"&command="+enc(cmd)+encodeArgs(args) + ajax_http_post("/cloud",req,cont,err) + }) +} + // Send a command to the GF shell function gfshell(cmd,cont) { with_dir(function(dir) { diff --git a/src/www/gfse/editor.css b/src/www/gfse/editor.css index 45199b72e..6d605b3ab 100644 --- a/src/www/gfse/editor.css +++ b/src/www/gfse/editor.css @@ -18,6 +18,8 @@ h1 img.nofloat { float: none; } div.home, div.grammar { border: 1px solid black; background: #9df; } div.home { padding: 5px; } div.files { margin: 0 8px 8px 8px; position: relative; } +td.public_grammars { padding-left: 2em; } +.no_publish .publish { display: none; } div#file, table.extension td, table.extension th { border: 2px solid #009; } div#file { border-top-width: 0; } @@ -32,6 +34,7 @@ table.extension td { border-left-width: 0; min-width: 30em; } .slideshow .hidden { display: none; } img.cloud, img.right, div.right, div.modtime { float: right; } + .modtime { color: #999; white-space: nowrap; } table.grammar_list { border-collapse: collapse; margin-left: 1.0em; } @@ -39,7 +42,8 @@ table.grammar_list td { padding: 0.4ex 0.25em; } div.namebar { background: #9df; } div.namebar table { width: 100%; } -.namebar h3, .home h3, .sheet h3 { margin: 0; color: #009; } +.namebar h3, .sheet h3 { margin: 0; color: #009; } +.home h3 { margin-top: 0; color: #009; } td.right { text-align: right; } td.center { text-align: center; } diff --git a/src/www/gfse/editor.js b/src/www/gfse/editor.js index c33f0bfa4..81901a3bd 100644 --- a/src/www/gfse/editor.js +++ b/src/www/gfse/editor.js @@ -6,10 +6,36 @@ var compiler_output=element("compiler_output") /* -------------------------------------------------------------------------- */ var grammar_cache=[] +var grammarlist={} function reget_grammar(ix) { return grammar_cache[ix]=local.get(ix) } function get_grammar(ix) { return grammar_cache[ix] || reget_grammar(ix) } -function put_grammar(ix,g) { grammar_cache[ix]=g; local.put(ix,g) } + +function put_grammar(ix,g) { + grammarlist[ix]={unique_name:g.unique_name,basename:g.basename} + grammar_cache[ix]=g; + local.put(ix,g) + local.put(".grammarlist",grammarlist) +} + +function remove_grammar(ix) { + delete grammar_cache[ix]; + delete grammarlist[ix]; + local.remove(ix); + local.put(".grammarlist",grammarlist) +} + +function my_grammar(unique_name) { + for(var ix in grammarlist) + if(grammarlist[ix].unique_name+".json"==unique_name) + return ix + return null +} + +// For sorting grammars alphabetically by name +function byBasename(a,b) { + return a.basename<b.basename ? -1 : a.basename>b.basename ? 1 : 0; +} /* -------------------------------------------------------------------------- */ @@ -32,8 +58,7 @@ function draw_grammar_list() { ? "Click to upload grammar updates to the cloud" : "Click to store your grammars in the cloud"}, [])]); - var home=div_class("home",[node("h3",{}, - [text("Your grammars"),cloud_upload])]); + var userlist=td(wrap("h3",[text("Your grammars "),cloud_upload])) if(uploaded) { var cloud_download= a(jsurl("download_json()"), @@ -43,33 +68,84 @@ function draw_grammar_list() { [])]); insertAfter(cloud_download,cloud_upload); } - editor.appendChild(home) + function edtr(cs) { return wrap_class("tr","extensible deletable",cs); } function del(i) { return function () { delete_grammar(i); } } - function clone(i) { return function (g,b) { clone_grammar(i); } } + function clone(i) { return function () { clone_grammar(i); } } function new_extension(i) { return function (g,b) { new_grammar([g]) }} - function item(i,grammar) { - var link=a(jsurl("open_grammar("+i+")"),[text(grammar.basename)]); - return node("tr",{"class":"extensible deletable"}, - [td(delete_button(del(i),"Delete this grammar")), - td(link), - td(more(grammar,clone(i),"Clone this grammar"))]) + function item(i,gid) { + //var i=my_grammar(gid.unique_name+".json") + var link=a(jsurl("open_grammar("+i+")"),[text(gid.basename)]); + if(!navigator.onLine) pubspan=[] + else { + function publish() { + pub.disabled=true + publish_json(get_grammar(i),draw_grammar_list) + } + var pub=attr({class:"public", + title:"Publish a copy of this grammar."}, + button("Publish",publish)) + var pubspan=[span_class("more",pub)] + } + return edtr([td(delete_button(del(i),"Delete this grammar")), + td(title(gid.unique_name,link)), + td(more(clone(i),"Clone this grammar")), + td(pubspan) + ]) } if(local.get("count",null)==null) - home.appendChild(text("You have not created any grammars yet.")); + userlist.appendChild(text("You have not created any grammars yet.")); else if(local.count==0) - home.appendChild(text("Your grammar list is empty.")); + userlist.appendChild(text("Your grammar list is empty.")); else { + //var ls=[]; + //for(var ix in grammarlist) ls.push(grammarlist[ix]) + //ls.sort(byBasename) var rows=[]; - for(var i=0;i<local.count;i++) { - var grammar=local.get(i,null); - if(grammar && grammar.basename) rows.push(item(i,grammar)) - } - home.appendChild(node("table",{"class":"grammar_list"},rows)); + for(var ix in grammarlist) rows.push(item(ix,grammarlist[ix])) + userlist.appendChild(node("table",{"class":"grammar_list"},rows)); } - home.appendChild(ul(li(a(jsurl("new_grammar()"),[text("New grammar")])))); + userlist.appendChild(ul(li(a(jsurl("new_grammar()"),[text("New grammar")])))); //editor.appendChild(text(local.count)); + var publiclist=empty_class("td","public_grammars") + function no_public() { + userlist.className="no_publish" + } + function show_public(files) { + function rmpublic(file) { + return function() { remove_public(file,draw_grammar_list) } + } + publiclist.appendChild(wrap("h3",text("Public grammars"))) + if(files.length>0) { + var unique_id=local.get("unique_id","-") + var t=empty_class("table","grammar_list") + for(var i in files) { + var file=files[i] + var parts=file.split("-") + var basename=parts[0] + var unique_name=parts[1]+"-"+parts[2] + var mine = my_grammar(unique_name)!=null + var del = mine + ? delete_button(rmpublic(file),"Don't publish this grammar") + : [] + var tit = mine + ? "This is a copy of your grammar" + : "Click to download a copy of this grammar" + t.appendChild(edtr([td(del), + td(title(tit, + a(jsurl('open_public("'+file+'")'), + [text(basename)])))])) + } + publiclist.appendChild(t) + } + else + publiclist.appendChild(p(text("No public grammars are available."))) + } + if(navigator.onLine) + gfcloud_public_json("ls",{},show_public,no_public) + var home=div_class("home",table(tr([userlist,publiclist]))) home.appendChild(empty_id("div","sharing")); + editor.appendChild(home) } function new_grammar(gs) { @@ -98,7 +174,7 @@ function empty_concretes_extending(gs) { } function remove_local_grammar(i) { - local.remove(i); + remove_grammar(i); while(local.count>0 && !local.get(local.count-1)) local.count--; } @@ -128,6 +204,21 @@ function open_grammar(i) { edit_grammar(g); } +function open_public(file) { + function got_json(g) { + delete g.index + delete g.unique_name + delete g.publishedAs + reload_grammar(g) + } + var parts=file.split("-") + var unique_name=parts[1]+"-"+parts[2] + var ix=my_grammar(unique_name) + console.log("open public",file,ix) + if(ix!=null) open_grammar(ix) + else gfcloud_public_json("download",{file:file},got_json) +} + function close_grammar(g) { clear(compiler_output); save_grammar(g); @@ -137,7 +228,7 @@ function reload_grammar(g) { save_grammar(g); edit_grammar(g); } function save_grammar(g) { if(g.index==null) g.index=local.count++; - local.put(g.index,g); + put_grammar(g.index,g); } function edit_grammar(g) { @@ -154,7 +245,8 @@ function draw_grammar(g) { break; default: g.view="column" - var files=div_class("files",[draw_filebar(g),draw_file(g)]); + var file=draw_file(g) + var files=div_class("files",[draw_filebar(g,file),file]); break; } return div_class("grammar",[draw_namebar(g,files),files]) @@ -185,14 +277,13 @@ function draw_name(g) { } function draw_closebutton(g) { - var b=button("X",function(){close_grammar(g);}); - b.title="Save and Close this grammar"; - return b; + return title("Save and Close this grammar", + button("X",function(){close_grammar(g);})) } function draw_view_button(g,view) { - var b=button(view,function(){open_view(g,view);}); - b.title="Shitch to the "+view+" view of the grammar"; + var b=title("Shitch to the "+view+" view of the grammar", + button(view,function(){open_view(g,view);})) b.disabled=g.view==view return b; } @@ -247,9 +338,8 @@ function compile_grammar(g,err_ind,cont) { function compile_button(g,err_ind) { function compile() { compile_grammar(g,err_ind) } - var b=button("Compile",compile); - b.title="Upload the grammar to the server to check it in GF for errors"; - return b; + return title("Upload the grammar to check it in GF for errors", + button("Compile",compile)); } function minibar_button(g,files,err_ind,comp_btn) { @@ -440,9 +530,8 @@ function minibar_button(g,files,err_ind,comp_btn) { } function goto_minibar_if_ok(res) { if(res.errorcode=="OK") goto_minibar(); } function compile() { compile_grammar(g,err_ind,goto_minibar_if_ok) } - var b=button("Minibar",compile); - b.title="Upload the grammar and test it in the minibar"; - return b; + var b=button("Minibar",compile) + return title("Upload the grammar and test it in the minibar",b) } function quiz_button(g,err_ind) { @@ -451,9 +540,8 @@ function quiz_button(g,err_ind) { location.href="../TransQuiz/translation_quiz.html?"+local.get("dir")+"/" } function compile() { compile_grammar(g,err_ind,goto_quiz) } - var b=button("Quiz",compile); - b.title="Upload the grammar and go to the translation quiz"; - return b; + return title("Upload the grammar and go to the translation quiz", + button("Quiz",compile)) } @@ -472,8 +560,7 @@ for(var i in languages) function concname(code) { return langname[code] || code; } -function add_concrete(g,el) { - var file=element("file"); +function add_concrete(g,file) { clear(file); var dc={}; for(var i in g.concretes) @@ -568,7 +655,7 @@ function conc_tab_button(g,ci,no_delete) { return no_delete ? b : deletable(del,b,"Delete this concrete syntax") } -function draw_filebar(g) { +function draw_filebar(g,file) { var cur=(g.current||0)-1; var filebar = empty_class("tr","extensible") filebar.appendChild(gap()); @@ -577,7 +664,8 @@ function draw_filebar(g) { filebar.appendChild(gap()); filebar.appendChild(tab(i==cur,conc_tab_button(g,i))); } - filebar.appendChild(td_gap(more(g,add_concrete,"Add a concrete syntax"))); + function add_conc(el) { return add_concrete(g,file) } + filebar.appendChild(td_gap(more(add_conc,"Add a concrete syntax"))); return wrap_class("table","tabs",filebar); } @@ -621,15 +709,15 @@ function draw_conc_extends(g,conc) { function draw_extends(g) { var kw_extends=kw("extends ",extends_hint) var exts= g.extends || []; - var m1=more(g,add_extends,"Inherit from other grammars"); - var m2=more(g,add_extends,"Inherit from more grammars"); var es=[exts.length>0 ? kw_extends : span_class("more",kw_extends)]; function del(i) { return function() { delete_extends(g,i); }} for(var i=0;i<exts.length;i++) { if(i>0) es.push(sep(", ")) es.push(deletable(del(i),ident(exts[i]),"Don't inherit from "+exts[i])); } - es.push(exts.length>0 ? m2 : m1); + var w= exts.length>0 ? "more" : "other" + function add_exts(el) { return add_extends(g); } + es.push(more(add_exts,"Inherit from "+w+" grammars")) return indent([extensible(es)]) } @@ -639,7 +727,7 @@ function delete_extends(g,ix) { reload_grammar(g); } -function add_extends(g,el) { +function add_extends(g) { var file=element("file"); clear(file) var gs=cached_grammar_array_byname(); @@ -772,7 +860,7 @@ function text_mode(g,file,ix) { return mode_button; } -function add_cat(g,el) { +function add_cat(g) { function add(s) { var cats=s.split(/\s*(?:\s|[;])\s*/); // allow separating spaces or ";" if(cats.length>0 && cats[cats.length-1]=="") cats.pop(); @@ -785,7 +873,7 @@ function add_cat(g,el) { reload_grammar(g); return null; } - string_editor(el,"",add); + return function(el) { string_editor(el,"",add); } } function delete_cat(g,ix) { @@ -836,11 +924,11 @@ function draw_cats(g) { es.push(draw_ecat(g,i,dc)); es.push(sep("; ")); } - es.push(more(g,add_cat,"Add more categories")); + es.push(more(add_cat(g),"Add more categories")); return es; } -function add_fun(g,el) { +function add_fun(g) { function add(s) { var p=parse_fun(s); if(p.ok) { @@ -852,7 +940,7 @@ function add_fun(g,el) { else return p.error } - string_editor(el,"",add); + return function(el) { string_editor(el,"",add);} } function edit_fun(i) { @@ -890,7 +978,7 @@ function draw_funs(g) { for(var i in funs) { es.push(node_sortable("fun",funs[i].name,[draw_efun(g,i,dc,df)])); } - es.push(more(g,add_fun,"Add a new function")); + es.push(more(add_fun(g),"Add a new function")); return es; } @@ -991,8 +1079,8 @@ var rgl_info = { Symbolic: "Functions for symbolic expressions (numbers and variables in mathematics)" } -function add_open(ci) { - return function (g,el) { +function add_open(g,ci) { + return function (el) { var conc=g.concretes[ci]; var os=conc.opens; var ds={}; @@ -1066,7 +1154,7 @@ function draw_opens(g,ci) { es.push(deletable(del(i),id,"Don't open this module")); first=false; } - es.push(more(g,add_open(ci),"Open more modules")); + es.push(more(add_open(g,ci),"Open more modules")); return indent(es); } @@ -1130,7 +1218,7 @@ function draw_params(g,ci) { es.push(div_class("param",[deletable(del(i),draw_eparam(i,dp),"Delete this parameter type")])); dp[params[i].name]=true; } - es.push(more(g,function(g,el) { return add_param(g,ci,el)}, + es.push(more(function(el) { return add_param(g,ci,el)}, "Add a new parameter type")); return indent(es); } @@ -1266,7 +1354,7 @@ function draw_opers(g,ci) { "Delete this operator definition")])); dp[opers[i].name]=true; } - es.push(more(g,function(g,el) { return add_oper(g,ci,el)}, + es.push(more(function(el) { return add_oper(g,ci,el)}, "Add a new operator definition")); function sort_opers() { conc.opers=sort_list(this,conc.opers,"name"); @@ -1385,7 +1473,7 @@ function draw_lins(g,ci) { // inherited grammars. // lintype :: Grammar -> Concrete -> [Grammar] -> {Cat=>ModId} => Type -> [Term] function lintype(g,conc,igs,dc,type) { - console.log(dc) + //console.log(dc) function ihcat_lincat(cat) { if(dc[cat]=="Predef") return "{s:Str}" // !!! Is this right? var ig=find_grammar_byname(igs,dc[cat]) @@ -1442,7 +1530,7 @@ function draw_matrix(g) { } t.appendChild(tr(row)) } - return div_class("extensible",[t,more(g,add_fun,"Add a new function")]) + return div_class("extensible",[t,more(add_fun(g),"Add a new function")]) } function simple_draw_lin(f) { @@ -1586,16 +1674,6 @@ function cleanup_deleted(files) { } } -function grammar_index() { - var index={} - var count=local.count - for(var i=0;i<count;i++) { - var g=local.get(i,null) - if(g && g.unique_name) index[g.unique_name]=i - } - return index -} - function merge_grammar(i,newg) { var oldg=get_grammar(i); var keep=""; @@ -1615,7 +1693,7 @@ function merge_grammar(i,newg) { } } } - local.put(i,newg) + put_grammar(i,newg) return keep; } @@ -1723,10 +1801,10 @@ function node_sortable(cls,name,ls) { function extensible(cs) { return div_class("extensible",cs); } -function more(g,action,hint,label) { +function more(action,hint,label) { var b=node("span",{"class":"more","title":hint || "Add more"}, [text(label || " + ")]); - b.onclick=function() { action(g,b); } + b.onclick=function() { action(b); } return b; } @@ -1775,6 +1853,12 @@ function table(rows) { return wrap("table",rows); } function td_right(cs) { return node("td",{"class":"right"},cs); } function td_center(cs) { return node("td",{"class":"center"},cs); } function jsurl(js) { return "javascript:"+js; } +function title(t,n) { return attr({title:t},n) } + +function attr(as,n) { + for(var a in as) n.setAttribute(a,as[a]); + return n +} /* -------------------------------------------------------------------------- */ @@ -1819,8 +1903,24 @@ function dir_bugfix() { else debug("No server directory yet") } +function get_grammarlist() { + var list=local.get(".grammarlist") + if(list) grammarlist=list + else if(local.get("count")!=null) { + grammarlist={} + var n=local.count; + for(var ix=0;ix<n;ix++) { + var g=local.get(ix,null); + if(g) + grammarlist[ix]={unique_name:g.unique_name,basename:g.basename} + } + local.put(".grammarlist",grammarlist) + } +} + if(editor) { if(supports_html5_storage()) { + get_grammarlist(); initial_view(); touch_edit(); dir_bugfix(); diff --git a/src/www/gfse/index.html b/src/www/gfse/index.html index 993f1a4d9..3b3543c2a 100644 --- a/src/www/gfse/index.html +++ b/src/www/gfse/index.html @@ -33,7 +33,7 @@ This page does not work without JavaScript. <hr> <div class=modtime><small> HTML -<!-- hhmts start --> Last modified: Thu Jun 21 16:40:51 CEST 2012 <!-- hhmts end --> +<!-- hhmts start -->Last modified: Wed Oct 3 23:44:31 CEST 2012 <!-- hhmts end --> </small></div> <a href="about.html">About</a> <pre id=debug></pre> @@ -43,8 +43,8 @@ HTML <script type="text/javascript" src="gf_abs.js"></script> <script type="text/javascript" src="example_based.js"></script> <script type="text/javascript" src="sort.js"></script> -<script type="text/javascript" src="editor.js"></script> <script type="text/javascript" src="cloud2.js"></script> +<script type="text/javascript" src="editor.js"></script> <script type="text/JavaScript" src="../minibar/minibar.js"></script> <script type="text/JavaScript" src="../minibar/minibar_input.js"></script> <script type="text/JavaScript" src="../minibar/minibar_translations.js"></script> diff --git a/src/www/translator/about.html b/src/www/translator/about.html index 1c0157945..0d790285b 100644 --- a/src/www/translator/about.html +++ b/src/www/translator/about.html @@ -54,6 +54,7 @@ closed and reopened later. Documents can be saved locally or in the cloud. to be capitalized, e.g. "I am ready." and "Spanish wine is good." <li>Document sharing in the cloud. <li>Interface to other translation services. + <li>Guided text entry, using the Minibar or some variant of it. <li>Interface to the grammar editor for grammar extension. <li>More browser compatibility testing (Chrome, Firefox, Safari & Opera Mobile tested so far). @@ -64,7 +65,7 @@ closed and reopened later. Documents can be saved locally or in the cloud. <hr> <div class=modtime><small> -<!-- hhmts start --> Last modified: Tue Jun 19 16:07:15 CEST 2012 <!-- hhmts end --> +<!-- hhmts start -->Last modified: Wed Oct 3 16:26:13 CEST 2012 <!-- hhmts end --> </small></div> <address> <a href="http://www.cse.chalmers.se/~hallgren/">TH</a> |
