swish/commit

New upstream files

authorJan Wielemaker
Tue Jul 24 14:13:26 2018 +0200
committerJan Wielemaker
Tue Jul 24 14:13:26 2018 +0200
commit3223dc1f108cbb51fd018b7b9498cb28784bfb30
tree336524a938cfae112891b4fa0aeaa13e656df0c1
parentcee4be58b96cd6b5c5cf37c9703074b1bb54678b
Diff style: patch stat
diff --git a/examples/htmlcell.swinb b/examples/htmlcell.swinb
index 77178d4..6fc397e 100644
--- a/examples/htmlcell.swinb
+++ b/examples/htmlcell.swinb
@@ -1,6 +1,6 @@
 <div class="notebook">
 
-<div class="nb-cell html">
+<div class="nb-cell html" name="htm1">
 <h2>Using HTML cells in SWISH notebooks</h2>
 
 <p>
@@ -17,12 +17,24 @@
   properties:
 </p>
 
+<style>
+  dl.htmlcell-doc dt { text-align: left; }
+  dt.htmlcell-doc-title { color: blue; margin-top: 5px;}
+</style>
 <div class="list-group">
-  <dl class="dl-horizontal">
-    <dt>.cell()</dt><dd>Returns a jQuery object pointing to the HTML cell
+  <dl class="dl-horizontal htmlcell-doc">
+    <dt class="htmlcell-doc-title">Finding objects</dt><dd>
+    </dd><dt>.cell([name])</dt><dd>Returns a jQuery object pointing to the named
+    or current HTML cell.
     </dd><dt>.notebook()</dt><dd>Returns a jQuery object of the entire notebook
     </dd><dt>.$(selector)</dt><dd>Returns a jQuery object holding all DOM elements
     matching <var>selector</var> in the current HTML cell.
+    </dd><dt class="htmlcell-doc-title">Running queries</dt><dd>
+    </dd><dt>.bindQuery([query,] f)</dt><dd>Bind the play button of the query named <var>query</var> to run the
+    function <var>f</var>.  If <var>query</var> is omitted, bind to the first query below this cell.
+    The function is called with a single argument that provides a method
+    <code>run(bindings)</code>, where <var>bindings</var> is an object where the keys are the names
+    of Prolog variables and the values are the values for these variables.
     </dd><dt>.run(query, parameters)</dt><dd>Run the named query cell.  <var>Parameters</var> is an object
     binding Prolog variables in the query to specified values.
     </dd><dt>.swish(options)</dt><dd>Wrapper around <code>new Pengine()</code> that fetches the sources
@@ -32,6 +44,11 @@
     jQuery selector), calls <code>options.predicate</code> with a single argument that is a dict that
     contains the fields of the form.  On success, <code>options.onsuccess</code> is called.  If an
     error occurs, this is displayed.
+    </dd><dt class="htmlcell-doc-title">Miscelleneous</dt><dd>
+    </dd><dt>.hideQuery(q[, on])</dt><dd>Hide the query editor and buttons for the query named <var>q</var> if
+    <var>on</var> is <code>true</code> (default).
+    </dd><dt>.loadCSS(url)</dt><dd>Load a CSS style sheet from <code>url</code> if this was not
+    already loaded.
   </dd></dl>
 </div>
 
@@ -47,7 +64,9 @@
   dynamically from the example sentences defined in the Prolog program at the
   end of the page.
 </p>
+</div>
 
+<div class="nb-cell html" name="htm2">
 <div class="panel panel-default">
   <div class="panel-body">
     <div class="form-group">
@@ -60,9 +79,6 @@
           </ul>
         </div>
         <input class="form-control">
-        <div class="input-group-btn">
-          <button type="button" class="btn btn-primary">Parse</button>
-        </div>
       </div>
     </div>
   </div>
@@ -95,13 +111,12 @@
     notebook.$("input").val($(this).text());
   });
 
-  // If the "Parse" button is clicked, run the query named "parse"
+  // If the play button of the "parse" query is clicked, run the query,
   // binding Sentence to the input string.  The function
-  // notebook.run() takes the name of a query and an object
-  // holding bindings.  This is translated to run the query
-  // Sentence = (String), (parse(Sentence, Tree)).
-  notebook.$(".btn-primary").on("click", function() {
-    notebook.run("parse", {Sentence: notebook.$("input").val()});
+  // notebook.bindQuery() takes the name of a query and a
+  // function that collects the arguments and calls the query.
+  notebook.bindQuery("parse", function(q) {
+    q.run({Sentence: notebook.$("input").val()});
   });
 </script>
 </div>
@@ -110,7 +125,7 @@
 parse(Sentence, Tree).
 </div>
 
-<div class="nb-cell markdown">
+<div class="nb-cell markdown" name="md1">
 ### The programs
 
 Below are three program fragments.  All three are declared as _background_ programs, so they are available to all queries posted from this notebook.  They specify
@@ -122,7 +137,7 @@ Below are three program fragments.  All three are declared as _background_ progr
 You can change the grammar as well as the example sentences and see the immediate effect.
 </div>
 
-<div class="nb-cell program" data-background="true">
+<div class="nb-cell program" data-background="true" name="p1">
 % A simple English DCG grammar
 % ============================
 
@@ -153,13 +168,13 @@ v(v(saw), _) --&gt; [saw].
 p(p(with)) --&gt; [with].
 </div>
 
-<div class="nb-cell program" data-background="true" data-singleline="true">
+<div class="nb-cell program" data-background="true" data-singleline="true" name="p2">
 example("john sees a man").
 example("a man sees john").
 example("john sees a man with a telescope").
 </div>
 
-<div class="nb-cell program" data-background="true" data-singleline="true">
+<div class="nb-cell program" data-background="true" data-singleline="true" name="p3">
 :- use_rendering(svgtree, [list(false)]).
 
 parse(Sentence, Tree) :-
diff --git a/examples/stats.swinb b/examples/stats.swinb
index cf35ce3..b018049 100644
--- a/examples/stats.swinb
+++ b/examples/stats.swinb
@@ -1,6 +1,6 @@
 <div class="notebook">
 
-<div class="nb-cell markdown">
+<div class="nb-cell markdown" name="md1">
 # Display SWISH server statistics
 
 This page examines the performance and health of the SWISH server.  Most of the statistics are gathered by `lib/swish_debug`, which is by default loaded into http://swish.swi-prolog.org but must be explicitly loaded into your own SWISH server.  Part of the statistics are based on reading the Linux =|/proc|= file system and thus only function on Linux.
@@ -8,18 +8,18 @@ This page examines the performance and health of the SWISH server.  Most of the
 The first step is easy, showing the overall statistics of the server.
 </div>
 
-<div class="nb-cell query">
+<div class="nb-cell query" name="q3">
 statistics.
 </div>
 
-<div class="nb-cell markdown">
+<div class="nb-cell markdown" name="md2">
 ## Historical performance statistics
 
 The charts below render historical performance characteristics of the server.  Please open the
 program below for a description of chart/3.
 </div>
 
-<div class="nb-cell program" data-singleline="true">
+<div class="nb-cell program" data-singleline="true" name="p1">
 :- use_rendering(c3).
 
 %%	chart(+Period, +Keys, -Chart) is det.
@@ -44,7 +44,8 @@ program below for a description of chart/3.
 %	    high if user applications use the dynamic database.
 %	  - `rss_mb`, `stack_mb`, `heap_mb` are the above divided by 1024^2.
 
-chart(Period, Keys, Chart) :-
+chart(PeriodS, Keys, Chart) :-
+    atom_string(Period, PeriodS),
     swish_stats(Period, Dicts0),
     maplist(add_heap_mb, Dicts0, Dicts1),
     maplist(rss_mb, Dicts1, Dicts2),
@@ -99,38 +100,110 @@ fix_date(Stat0, Stat) :-
     put_dict(time, Stat0, Time, Stat).
 </div>
 
-<div class="nb-cell markdown">
-### Number of Pengines and CPU load over the past hour
-
-The number of Pegines denotes the number of actively executing queries.  These queries may be sleeping while waiting for input, a debugger command or the user asking for more answers. Note that the number of Pengines is sampled and short-lived Pengines does not appear in this chart.
-</div>
-
-<div class="nb-cell query">
-chart(hour, [pengines,d_cpu], Chart).
-</div>
-
-<div class="nb-cell markdown">
-### Number of threads and visitors
-
-Threads are used as HTTP workers, pengines and some adminstrative tasks.  Visitors is the number of open websockets, which reflects the number of browser windows watching this page.
-</div>
-
-<div class="nb-cell query">
-chart(hour, [pengines,threads,visitors], Chart).
-</div>
-
-<div class="nb-cell markdown">
-### Memory usage over the past hour
-
-*rss* is the total (_resident_) memory usage as reported by Linux.  *stack* is the memory occupied by all Prolog stacks.
-*heap* is an approximation of the memory used for the Prolog program space, computed as _rss_ - _stack_ - _free_.  This is incorrect for two reasons.  It ignores the C-stacks and the not-yet-committed memory of the Prolog stacks is not part of *rss*.  *free* is memory that is freed but not yet reused as reported by GNU =|malinfo()|= as `fordblks`.
-</div>
-
-<div class="nb-cell query">
-chart(hour, [rss_mb,heap_mb,stack_mb,free_mb], Chart).
-</div>
-
-<div class="nb-cell markdown">
+<div class="nb-cell html" name="htm1">
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <label>Number of Pengines and CPU load over the past period</label>
+  </div>
+  <p>The number of Pegines denotes the number of actively executing queries.
+     These queries may be sleeping while waiting for input, a debugger command
+     or the user asking for more answers. Note that the number of Pengines is
+     sampled and short-lived Pengines does not appear in this chart.
+  </p><div class="panel-body">
+    <div class="form-group row" style="margin-bottom:0px">
+      <label class="col-sm-2">Period:</label>
+      <div class="col-sm-10">
+        <label class="radio-inline"><input name="period1" value="week" type="radio">Week</label>
+        <label class="radio-inline"><input name="period1" value="day" type="radio">Day</label>
+        <label class="radio-inline"><input name="period1" value="hour" checked="" type="radio">Hour</label>
+        <label class="radio-inline"><input name="period1" value="minute" type="radio">Minute</label>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  notebook.bindQuery(function(q) {
+    q.run({ Period: notebook.$('input[type=radio]:checked').val() });
+  });
+</script>
+</div>
+
+<div class="nb-cell query" name="cpu">
+projection([Chart]),
+chart(Period, [pengines,d_cpu], Chart).
+</div>
+
+<div class="nb-cell html" name="htm2">
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <label>Number of threads and visitors</label>
+  </div>
+  <p>Threads are used as HTTP workers, pengines and some adminstrative tasks.
+     Visitors is the number of open websockets, which reflects the number of browser
+     windows watching this page.
+  </p><div class="panel-body">
+    <div class="form-group row" style="margin-bottom:0px">
+      <label class="col-sm-2">Period:</label>
+      <div class="col-sm-10">
+        <label class="radio-inline"><input name="period2" value="week" type="radio">Week</label>
+        <label class="radio-inline"><input name="period2" value="day" type="radio">Day</label>
+        <label class="radio-inline"><input name="period2" value="hour" checked="" type="radio">Hour</label>
+        <label class="radio-inline"><input name="period2" value="minute" type="radio">Minute</label>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  notebook.bindQuery(function(q) {
+    q.run({ Period: notebook.$('input[type=radio]:checked').val() });
+  });
+</script>
+</div>
+
+<div class="nb-cell query" name="visitors">
+projection([Chart]),
+chart(Period, [pengines,threads,visitors], Chart).
+</div>
+
+<div class="nb-cell html" name="htm3">
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <label>Memory usage over the past hour</label>
+  </div>
+  <p><b>rss</b> is the total (resident) memory usage as reported by Linux. <b>stack</b> is the memory
+    occupied by all Prolog stacks. <b>heap</b> is an approximation of the memory used for the
+     Prolog program space, computed as <i>rss - stack - free<i>. This is incorrect for two reasons.
+     It ignores the C-stacks and the not-yet-committed memory of the Prolog stacks
+     is not part of rss. free is memory that is freed but not yet reused as reported
+    by GNU <a href="https://www.gnu.org/software/libc/manual/html_node/Statistics-of-Malloc.html">malinfo()</a> as <code>fordblks</code>.
+  </i></i></p><div class="panel-body"><i><i>
+    <div class="form-group row" style="margin-bottom:0px">
+      <label class="col-sm-2">Period:</label>
+      <div class="col-sm-10">
+        <label class="radio-inline"><input name="period3" value="week" type="radio">Week</label>
+        <label class="radio-inline"><input name="period3" value="day" type="radio">Day</label>
+        <label class="radio-inline"><input name="period3" value="hour" checked="" type="radio">Hour</label>
+        <label class="radio-inline"><input name="period3" value="minute" type="radio">Minute</label>
+      </div>
+    </div>
+  </i></i></div><i><i>
+</i></i></div><i><i>
+
+<script>
+  notebook.bindQuery(function(q) {
+    q.run({ Period: notebook.$('input[type=radio]:checked').val() });
+  });
+</script></i></i>
+</div>
+
+<div class="nb-cell query" name="q6">
+projection([Chart]),
+chart(Period, [rss_mb,heap_mb,stack_mb,free_mb], Chart).
+</div>
+
+<div class="nb-cell markdown" name="md6">
 ## Health statistics
 
 The statistics below assesses the number of *Pengines* (actively executing queries from users) and the *highlight states*, the number of server-side mirrors we have from client's source code used to compute the semantically enriched tokens.   If such states are not explicitly invalidated by the client, they are removed after having not been accessed for one hour.  The *stale modules* count refers to temporary modules that are not associated to a Pengine, nor to a highlight state and probably indicate a leak.
@@ -138,7 +211,7 @@ The statistics below assesses the number of *Pengines* (actively executing queri
 The two queries below extract information about stale modules and threads that have died.  These are used to help debugging related leaks.
 </div>
 
-<div class="nb-cell program" data-singleline="true">
+<div class="nb-cell program" data-singleline="true" name="p2">
 :- use_rendering(table).
 
 stats([stale_modules-Stale|Pairs]) :-
@@ -148,7 +221,7 @@ stats([stale_modules-Stale|Pairs]) :-
             Pairs).
 </div>
 
-<div class="nb-cell query">
+<div class="nb-cell query" name="q7">
 stats(Stats).
 </div>
 
diff --git a/examples/swish_tutorials.swinb b/examples/swish_tutorials.swinb
index 185a4d1..ab59aaf 100644
--- a/examples/swish_tutorials.swinb
+++ b/examples/swish_tutorials.swinb
@@ -6,7 +6,10 @@
 This notebook provides an overview of tutorials about using SWISH.
 
   - [Rendering answers graphically](example/rendering.swinb)
-  - [Using HTML cells in notebooks](example/htmlcell.swinb)
+  - Using HTML cells in notebooks
+    - [The basics](example/htmlcell.swinb)
+    - [Including user widgets (slider)](example/slider.swinb)
+    - [Hiding all SWISH elements](example/chat80.swinb)
   - [Accessing external data](example/data_source.swinb)
   - [Access the SWISH interface from Prolog](example/jquery.swinb)
 </div>
diff --git a/lib/swish/chat.pl b/lib/swish/chat.pl
index 1a54fe7..16d6e8c 100644
--- a/lib/swish/chat.pl
+++ b/lib/swish/chat.pl
@@ -152,7 +152,7 @@ extend_options([_|T0], Options, T) :-
 %	See whether the client associated with  a session is flooding us
 %	and if so, return a resource error.
 
-check_flooding(_0Session) :-
+check_flooding(Session) :-
 	get_time(Now),
 	(   http_session_retract(websocket(Score, Last))
 	->  Passed is Now-Last,
@@ -161,11 +161,13 @@ check_flooding(_0Session) :-
 	    Passed = 0
 	),
 	debug(chat(flooding), 'Flooding score: ~2f (session ~p)',
-	      [NewScore, _0Session]),
+	      [NewScore, Session]),
 	http_session_assert(websocket(NewScore, Now)),
 	(   NewScore > 50
 	->  throw(http_reply(resource_error(
-				 websocket(reconnect(Passed, NewScore)))))
+				 error(permission_error(reconnect, websocket,
+							Session),
+				       websocket(reconnect(Passed, NewScore))))))
 	;   true
 	).
 
@@ -387,10 +389,19 @@ do_gc_visitors :-
 
 reclaim_visitor(WSID) :-
 	debug(chat(gc), 'Reclaiming idle ~p', [WSID]),
-	retractall(visitor_session(WSID, _Session, _Token)),
+	reclaim_visitor_session(WSID),
 	retractall(visitor_status(WSID, _Status)),
 	unsubscribe(WSID, _).
 
+reclaim_visitor_session(WSID) :-
+	forall(retract(visitor_session(WSID, Session, _Token)),
+		       http_session_retractall(websocket(_, _), Session)).
+
+:- if(\+current_predicate(http_session_retractall/2)).
+http_session_retractall(Data, Session) :-
+	retractall(http_session:session_data(Session, Data)).
+:- endif.
+
 
 %%	create_session_user(+Session, -User, -UserData, +Options)
 %
@@ -747,6 +758,11 @@ avatar_property(_Avatar, Source, avatar_source, Source).
 %	HTTP handler for Noble  Avatar   images.  Using  create_avatar/2
 %	re-creates avatars from the file name,  so we can safely discard
 %	the avatar file store.
+%
+%	Not really. A new user gets a new   avatar  and this is based on
+%	whether or not the file exists. Probably we should maintain a db
+%	of handed out avatars and their last-use   time stamp. How to do
+%	that? Current swish stats: 400K avatars, 3.2Gb data.
 
 reply_avatar(Request) :-
 	option(path_info(Local), Request),
@@ -1316,8 +1332,8 @@ broadcast_bell(_Options) -->
 		 *******************************/
 
 :- multifile
-	prolog:message//1.
+	prolog:message_context//1.
 
-prolog:message(websocket(reconnect(Passed, Score))) -->
+prolog:message_context(websocket(reconnect(Passed, Score))) -->
 	[ 'WebSocket: too frequent reconnect requests (~1f sec; score = ~1f)'-
 	  [Passed, Score] ].
diff --git a/lib/swish/config.pl b/lib/swish/config.pl
index 4799d63..39f402f 100644
--- a/lib/swish/config.pl
+++ b/lib/swish/config.pl
@@ -3,7 +3,8 @@
     Author:        Jan Wielemaker
     E-mail:        J.Wielemaker@vu.nl
     WWW:           http://www.swi-prolog.org
-    Copyright (c)  2014-2016, VU University Amsterdam
+    Copyright (c)  2014-2018, VU University Amsterdam
+			      CWI, Amsterdam
     All rights reserved.
 
     Redistribution and use in source and binary forms, with or without
@@ -38,12 +39,15 @@
 	    swish_config_hash/2		% -HASH, +Options
 	  ]).
 :- use_module(library(http/http_dispatch)).
+:- use_module(library(http/http_path)).
 :- use_module(library(http/http_json)).
 :- use_module(library(option)).
+:- use_module(library(apply)).
 
 :- multifile
 	config/2,			% ?Key, ?Value
 	config/3,			% ?Key, ?Value, +Options
+	web_plugin/1,			% ?Dict
 	source_alias/2,			% ?Alias, ?Options
 	authenticate/2,			% +Request, -User
         login_item/2,                   % -Server, -HTML_DOM
@@ -78,10 +82,12 @@ swish_config_hash(Hash, Options) :-
 
 json_config(json{ http: json{ locations:JSON
 			    },
-		  swish: SWISHConfig
+		  swish: SWISHConfig,
+		  plugins : Plugins
 		}, Options) :-
 	http_locations(JSON),
-	swish_config_dict(SWISHConfig, Options).
+	swish_config_dict(SWISHConfig, Options),
+	web_plugins(Plugins, Options).
 
 http_locations(JSON) :-
 	findall(ID-Path,
@@ -116,7 +122,53 @@ same_ids(T, _, T, []).
 
 swish_config_dict(Config, Options) :-
 	findall(Key-Value, swish_config(Key, Value, Options), Pairs),
-	dict_pairs(Config, json, Pairs).
+	keysort(Pairs, Sorted),
+	warn_duplicate_config(Sorted, Unique),
+	dict_pairs(Config, json, Unique).
+
+:- dynamic  warned_duplicate/1.
+:- volatile warned_duplicate/1.
+
+warn_duplicate_config([], []).
+warn_duplicate_config([K-V1,K-V2|T0], [K-V1|T]) :- !,
+	collect_same(K, T0, VL, T1),
+	(   warned_duplicate(K)
+	->  true
+	;   print_message(warning, swish(duplicate_config(K, [V1,V2|VL]))),
+	    assertz(warned_duplicate(K))
+	),
+	warn_duplicate_config(T1, T).
+warn_duplicate_config([KV|T0], [KV|T]) :- !,
+	warn_duplicate_config(T0, T).
+
+collect_same(K, [K-V|T0], [V|VT], T) :- !,
+	collect_same(K, T0, VT, T).
+collect_same(_, List, [], List).
+
+%!	web_plugins(-Plugins, +Options) is det.
+%
+%	Obtain a list of JSON dicts for additional web plugins.
+
+web_plugins(Plugins, _Options) :-
+	findall(Plugin, web_plugin_ex(Plugin), Plugins).
+
+web_plugin_ex(Plugin) :-
+	web_plugin(Plugin0),
+	dict_pairs(Plugin0, Tag, Pairs0),
+	maplist(expand_paths, Pairs0, Pairs),
+	dict_pairs(Plugin, Tag, Pairs).
+
+:- multifile http:location/3.
+:- dynamic   http:location/3.
+
+expand_paths(Name-Spec, Name-Path) :-
+	compound(Spec),
+	compound_name_arity(Spec, Alias, 1),
+	http:location(Alias, _, _),
+	!,
+	http_absolute_location(Spec, Path, []).
+expand_paths(Pair, Pair).
+
 
 %%	config(-Key, -Value) is nondet.
 %%	swish_config(-Key, -Value) is nondet.
@@ -236,6 +288,10 @@ config(residuals_var, '_residuals').
 prolog:message(http(duplicate_handlers(Id, Paths))) -->
 	[ 'Duplicate HTTP handler IDs: "~w"'-[Id] ],
 	paths(Paths).
+prolog:message(swish(duplicate_config(K, [V0|List]))) -->
+	[ 'Duplicate SWISH config values for "~w": ~p.  Using ~q'-
+	  [K, [V0|List], V0]
+	].
 
 paths([]) --> [].
 paths([H|T]) --> [ '\t~q'-[H], nl ], paths(T).
diff --git a/lib/swish/pack/wordnet/pack.pl b/lib/swish/pack/wordnet/pack.pl
index abe8bf2..da01308 100644
--- a/lib/swish/pack/wordnet/pack.pl
+++ b/lib/swish/pack/wordnet/pack.pl
@@ -1,5 +1,5 @@
 name(wordnet).
-version('0.9.1').
+version('0.9.3').
 title('Access to WordNet database').
 keywords([wordnet, lexical, nlp]).
 author( 'Jan Wielemaker', 'jan@swi-prolog.org' ).
diff --git a/lib/swish/pack/wordnet/prolog/wn.pl b/lib/swish/pack/wordnet/prolog/wn.pl
index 4c3b88c..eb71597 100644
--- a/lib/swish/pack/wordnet/prolog/wn.pl
+++ b/lib/swish/pack/wordnet/prolog/wn.pl
@@ -84,11 +84,11 @@ Some more remarks:
    both adjective and adjective_satellite are represented as
    3XXXXXXXX
 
-@author Originally by Jan Wielemaker.  Partly documented by an
-unknown author.  Current commens copied from prologdb.5WN.html
-file from the sources.
-@see Wordnet is a lexical database for the English language. See
-http://www.cogsci.princeton.edu/~wn/
+@author Originally by Jan Wielemaker. Partly documented by Samer
+Abdallah. Current comments copied from prologdb.5WN.html file from the
+sources.
+@see Wordnet is a lexical database for the English language.
+See http://www.cogsci.princeton.edu/~wn/
 */
 
 
@@ -418,22 +418,9 @@ load_wordnet :-
 
 load_op(Name) :-
 	atom_concat('wn_', Name, File),
-	absolute_file_name(wndb(File),
-			   [ access(read),
-			     file_type(prolog)
-			   ],
-			   PlFile),
-	file_name_extension(Base, _Ext, PlFile),
-	file_name_extension(Base, qlf, QlfFile),
-	(   exists_file(QlfFile),
-	    time_file(QlfFile, QlfTime),
-	    time_file(PlFile, PlTime),
-	    QlfTime >= PlTime
-	->  load_files(QlfFile)
-	;   access_file(QlfFile, write)
-	->  qcompile(PlFile)
-	;   load_files(PlFile)
-	).
+        load_files(wndb(File),
+                   [ qcompile(auto)
+                   ]).
 
 
 		 /*******************************
diff --git a/lib/swish/page.pl b/lib/swish/page.pl
index f25ad8a..c8a21bd 100644
--- a/lib/swish/page.pl
+++ b/lib/swish/page.pl
@@ -3,7 +3,8 @@
     Author:        Jan Wielemaker
     E-mail:        J.Wielemaker@vu.nl
     WWW:           http://www.swi-prolog.org
-    Copyright (c)  2014-2017, VU University Amsterdam
+    Copyright (c)  2014-2018, VU University Amsterdam
+			      CWI, Amsterdam
     All rights reserved.
 
     Redistribution and use in source and binary forms, with or without
@@ -87,6 +88,7 @@ http:location(pldoc, swish(pldoc), [priority(100)]).
 
 :- multifile
 	swish_config:logo//1,
+	swish_config:title//1,
 	swish_config:source_alias/2,
 	swish_config:reply_page/1,
 	swish_config:li_login_button//1.
@@ -158,14 +160,7 @@ swish_reply3(_, Options) :-
 swish_reply3(_, Options) :-
 	reply_html_page(
 	    swish(main),
-	    [ title('SWISH -- SWI-Prolog for SHaring'),
-	      link([ rel('shortcut icon'),
-		     href('/icons/favicon.ico')
-		   ]),
-	      link([ rel('apple-touch-icon'),
-		     href('/icons/swish-touch-icon.png')
-		   ])
-	    ],
+	    \swish_title(Options),
 	    \swish_page(Options)).
 
 params_options([], []).
@@ -389,6 +384,33 @@ collapsed_button -->
 		      span(class('icon-bar'), [])
 		    ])).
 
+
+		 /*******************************
+		 *	      BRANDING		*
+		 *******************************/
+
+%!	swish_title(+Options)// is det.
+%
+%	Emit the HTML header options dealing with the title and shortcut
+%	icons.  This can be hooked using swish_config:title//1.
+
+swish_title(Options) -->
+	swish_config:title(Options), !.
+swish_title(_Options) -->
+	html([ title('SWISH -- SWI-Prolog for SHaring'),
+	       link([ rel('shortcut icon'),
+		      href('/icons/favicon.ico')
+		    ]),
+	       link([ rel('apple-touch-icon'),
+		      href('/icons/swish-touch-icon.png')
+		    ])
+	     ]).
+
+%!	swish_logos(+Options)// is det.
+%
+%	Emit the navbar branding logos at   the  top-left. Can be hooked
+%	using swish_config:swish_logos//1.
+
 swish_logos(Options) -->
 	swish_config:logo(Options), !.
 swish_logos(Options) -->
@@ -399,7 +421,8 @@ swish_logos(Options) -->
 %
 %	Hook  to  include  the  top-left    logos.   The  default  calls
 %	pengine_logo//1 and swish_logo//1.  The   implementation  should
-%	emit zero or more <a> elements.
+%	emit     zero     or      more       <a>      elements.      See
+%	`config_available/branding.pl` for an example.
 
 %!	pengine_logo(+Options)// is det.
 %!	swish_logo(+Options)// is det.
@@ -418,6 +441,10 @@ swish_logo(_Options) -->
 	html(a([href(HREF), class('swish-logo')], &(nbsp))).
 
 
+		 /*******************************
+		 *	     CONTENT		*
+		 *******************************/
+
 %%	swish_content(+Options)//
 %
 %	Generate the SWISH editor, Prolog output  area and query editor.
diff --git a/lib/swish/paths.pl b/lib/swish/paths.pl
index 2d3364b..e7559c9 100644
--- a/lib/swish/paths.pl
+++ b/lib/swish/paths.pl
@@ -52,9 +52,12 @@ user:file_search_path(config,         config_enabled(.)).
 user:file_search_path(config,         swish('config-available')).
 user:file_search_path(swish_web,      swish(web)).
 user:file_search_path(swish_pack,     swish(pack)).
+user:file_search_path(js,             config('web/js')).
 user:file_search_path(js,             swish_web(js)).
 user:file_search_path(css,            swish_web(css)).
+user:file_search_path(icons,          config('web/icons')).
 user:file_search_path(icons,          swish_web(icons)).
+user:file_search_path(plugin,         config('web/plugin')).
 
 %!  set_swish_path
 %
diff --git a/lib/swish/plugin/notify.pl b/lib/swish/plugin/notify.pl
index e8ab9ee..10aa14e 100644
--- a/lib/swish/plugin/notify.pl
+++ b/lib/swish/plugin/notify.pl
@@ -300,11 +300,11 @@ notify_event(follow(DocID, ProfileID, Options)) :-
     follow(DocID, ProfileID, Options).
 % events on gitty files
 notify_event(updated(File, Commit)) :-
-    (   storage_meta_data(Commit.get(previous), OldCommit),
-        atom_concat('gitty:', OldCommit.name, DocID)
-    ->  notify(DocID, forked(OldCommit, Commit))
+    storage_meta_data(Commit.get(previous), OldCommit),
+    (   atom_concat('gitty:', OldCommit.name, DocID)
+    ->  notify(DocID, updated(Commit))
     ;   atom_concat('gitty:', File, DocID),
-        notify(DocID, updated(Commit))
+        notify(DocID, forked(OldCommit, Commit))
     ).
 notify_event(deleted(File, Commit)) :-
     atom_concat('gitty:', File, DocID),
diff --git a/lib/swish/swish_csv.pl b/lib/swish/swish_csv.pl
index 89ca341..a2db2c9 100644
--- a/lib/swish/swish_csv.pl
+++ b/lib/swish/swish_csv.pl
@@ -113,6 +113,7 @@ success(Answers, VarTerm, Options) :-
 	       csv_write_stream(current_output, Page, [])).
 
 projection_row(-) :- !.
+projection_row(row) :- !.
 projection_row(VarTerm) :-
 	csv_write_stream(current_output, [VarTerm], []).
 
diff --git a/web/bower_components/codemirror/mode/javascript/javascript.js b/web/bower_components/codemirror/mode/javascript/javascript.js
index c4a709c..ba27c5d 100644
--- a/web/bower_components/codemirror/mode/javascript/javascript.js
+++ b/web/bower_components/codemirror/mode/javascript/javascript.js
@@ -75,17 +75,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
       return ret(ch);
     } else if (ch == "=" && stream.eat(">")) {
       return ret("=>", "operator");
-    } else if (ch == "0" && stream.eat(/x/i)) {
-      stream.eatWhile(/[\da-f]/i);
-      return ret("number", "number");
-    } else if (ch == "0" && stream.eat(/o/i)) {
-      stream.eatWhile(/[0-7]/i);
-      return ret("number", "number");
-    } else if (ch == "0" && stream.eat(/b/i)) {
-      stream.eatWhile(/[01]/i);
+    } else if (ch == "0" && stream.match(/^(?:x[\da-f]+|o[0-7]+|b[01]+)n?/i)) {
       return ret("number", "number");
     } else if (/\d/.test(ch)) {
-      stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
+      stream.match(/^\d*(?:n|(?:\.\d*)?(?:[eE][+\-]?\d+)?)?/);
       return ret("number", "number");
     } else if (ch == "/") {
       if (stream.eat("*")) {
@@ -96,7 +89,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
         return ret("comment", "comment");
       } else if (expressionAllowed(stream, state, 1)) {
         readRegexp(stream);
-        stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
+        stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
         return ret("regexp", "string-2");
       } else {
         stream.eat("=");
@@ -265,21 +258,42 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
     pass.apply(null, arguments);
     return true;
   }
+  function inList(name, list) {
+    for (var v = list; v; v = v.next) if (v.name == name) return true
+    return false;
+  }
   function register(varname) {
-    function inList(list) {
-      for (var v = list; v; v = v.next)
-        if (v.name == varname) return true;
-      return false;
-    }
     var state = cx.state;
     cx.marked = "def";
     if (state.context) {
-      if (inList(state.localVars)) return;
-      state.localVars = {name: varname, next: state.localVars};
+      if (state.lexical.info == "var" && state.context && state.context.block) {
+        // FIXME function decls are also not block scoped
+        var newContext = registerVarScoped(varname, state.context)
+        if (newContext != null) {
+          state.context = newContext
+          return
+        }
+      } else if (!inList(varname, state.localVars)) {
+        state.localVars = new Var(varname, state.localVars)
+        return
+      }
+    }
+    // Fall through means this is global
+    if (parserConfig.globalVars && !inList(varname, state.globalVars))
+      state.globalVars = new Var(varname, state.globalVars)
+  }
+  function registerVarScoped(varname, context) {
+    if (!context) {
+      return null
+    } else if (context.block) {
+      var inner = registerVarScoped(varname, context.prev)
+      if (!inner) return null
+      if (inner == context.prev) return context
+      return new Context(inner, context.vars, true)
+    } else if (inList(varname, context.vars)) {
+      return context
     } else {
-      if (inList(state.globalVars)) return;
-      if (parserConfig.globalVars)
-        state.globalVars = {name: varname, next: state.globalVars};
+      return new Context(context.prev, new Var(varname, context.vars), false)
     }
   }
 
@@ -289,15 +303,23 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 
   // Combinators
 
-  var defaultVars = {name: "this", next: {name: "arguments"}};
+  function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
+  function Var(name, next) { this.name = name; this.next = next }
+
+  var defaultVars = new Var("this", new Var("arguments", null))
   function pushcontext() {
-    cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
-    cx.state.localVars = defaultVars;
+    cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
+    cx.state.localVars = defaultVars
+  }
+  function pushblockcontext() {
+    cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
+    cx.state.localVars = null
   }
   function popcontext() {
-    cx.state.localVars = cx.state.context.vars;
-    cx.state.context = cx.state.context.prev;
+    cx.state.localVars = cx.state.context.vars
+    cx.state.context = cx.state.context.prev
   }
+  popcontext.lex = true
   function pushlex(type, info) {
     var result = function() {
       var state = cx.state, indent = state.indented;
@@ -322,19 +344,19 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
   function expect(wanted) {
     function exp(type) {
       if (type == wanted) return cont();
-      else if (wanted == ";") return pass();
+      else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
       else return cont(exp);
     };
     return exp;
   }
 
   function statement(type, value) {
-    if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
+    if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
     if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
     if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
     if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
     if (type == "debugger") return cont(expect(";"));
-    if (type == "{") return cont(pushlex("}"), block, poplex);
+    if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
     if (type == ";") return cont();
     if (type == "if") {
       if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
@@ -363,18 +385,20 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
         return cont(pushlex("stat"), maybelabel);
       }
     }
-    if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"),
-                                      block, poplex, poplex);
+    if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
+                                      block, poplex, poplex, popcontext);
     if (type == "case") return cont(expression, expect(":"));
     if (type == "default") return cont(expect(":"));
-    if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
-                                     statement, poplex, popcontext);
+    if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
     if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
     if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
     if (type == "async") return cont(statement)
     if (value == "@") return cont(expression, statement)
     return pass(pushlex("stat"), expression, expect(";"), poplex);
   }
+  function maybeCatchBinding(type) {
+    if (type == "(") return cont(funarg, expect(")"))
+  }
   function expression(type, value) {
     return expressionInner(type, value, false);
   }
@@ -783,7 +807,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
         cc: [],
         lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
         localVars: parserConfig.localVars,
-        context: parserConfig.localVars && {vars: parserConfig.localVars},
+        context: parserConfig.localVars && new Context(null, null, false),
         indented: basecolumn || 0
       };
       if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
@@ -824,7 +848,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
         lexical = lexical.prev;
       var type = lexical.type, closing = firstChar == type;
 
-      if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
+      if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
       else if (type == "form" && firstChar == "{") return lexical.indented;
       else if (type == "form") return lexical.indented + indentUnit;
       else if (type == "stat")