Merge branch 'master' of gh:apenwarr/afterquery

* 'master' of gh:apenwarr/afterquery:
  Convert bool columns to numbers when charting.
  Add afterquery.exec(), which calculates a grid but doesn't render it.
  getUrlData() is now run as part of the queue.
  Change enqueue()/runqueue() to avoid global variables.
  wvtest.js: bare minimum changes to make it work with MacOS jsc command.
diff --git a/.gitignore b/.gitignore
index 265b1b2..38d2692 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,4 @@
 .*~
 *.pyc
 help.html
-v8shell
+jsshell
diff --git a/Makefile b/Makefile
index 4c3e927..6a6b181 100644
--- a/Makefile
+++ b/Makefile
@@ -2,8 +2,12 @@
 
 all: help.html
 
-v8shell: v8shell.cc
-	g++ -o $@ $< -lv8
+MACOS_JS_PATH=/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Resources/jsc
+jsshell:
+	rm -f $@
+	[ -e "${MACOS_JS_PATH}" ] && \
+	ln -s "${MACOS_JS_PATH}" jsshell || \
+	g++ -o $@ v8shell.cc -lv8
 
 runtests: $(patsubst %.js,%.js.run,$(wildcard t/t*.js))
 
@@ -11,12 +15,12 @@
 	markdown $< >$@.new
 	mv $@.new $@
 
-%.js.run: %.js v8shell
-	./v8shell wvtest.js $*.js
+%.js.run: %.js jsshell
+	./jsshell wvtest.js $*.js
 
-test: v8shell
+test: jsshell
 	./wvtestrun $(MAKE) runtests
 
 clean:
-	rm -f *~ .*~ */*~ */.*~ help.html v8shell
+	rm -f *~ .*~ */*~ */.*~ help.html v8shell jsshell
 	find . -name '*~' -exec rm -f {} \;
diff --git a/edit.html b/edit.html
index 8e753ba..a0083ad 100644
--- a/edit.html
+++ b/edit.html
@@ -90,8 +90,7 @@
 lastloaded = null;
 var exampleurl = (window.location.protocol + '//' + window.location.host +
                   '/example1.json');
-args = afterquery.internal.parseArgs(window.location.search ||
-                                     ('?url=' + exampleurl));
+args = afterquery.parseArgs(window.location.search || ('?url=' + exampleurl));
 argsToForm(args);
 updateUrl();
 </script>
diff --git a/render.js b/render.js
index 18b0e9f..31579b7 100644
--- a/render.js
+++ b/render.js
@@ -61,7 +61,17 @@
 
 
   function parseArgs(query) {
-    var kvlist = query.substr(1).split('&');
+    var kvlist;
+    if (query.join) {
+      // user provided an array of 'key=value' strings
+      kvlist = query;
+    } else {
+      // assume user provided a single string
+      if (query[0] == '?' || query[0] == '#') {
+        query = query.substr(1);
+      }
+      kvlist = query.split('&');
+    }
     var out = {};
     var outlist = [];
     for (var i in kvlist) {
@@ -120,7 +130,7 @@
       dheaders.push({
         id: headers[i],
         label: headers[i],
-        type: types[i]
+        type: (types[i] != T_BOOL || !options.bool_to_num) ? types[i] : T_NUM
       });
     }
     var ddata = [];
@@ -1025,14 +1035,19 @@
   }
 
 
-  function gridFromData(gotdata) {
+  function gridFromData(rawdata) {
+    if (rawdata && rawdata.headers && rawdata.data && rawdata.types) {
+      // already in grid format
+      return rawdata;
+    }
+
     var headers, data, types;
 
     var err;
-    if (gotdata.errors && gotdata.errors.length) {
-      err = gotdata.errors[0];
-    } else if (gotdata.error) {
-      err = gotdata.error;
+    if (rawdata.errors && rawdata.errors.length) {
+      err = rawdata.errors[0];
+    } else if (rawdata.error) {
+      err = rawdata.error;
     }
     if (err) {
       var msglist = [];
@@ -1041,16 +1056,16 @@
       throw new Error('Data provider returned an error: ' + msglist.join(': '));
     }
 
-    if (gotdata.table) {
+    if (rawdata.table) {
       // gviz format
       headers = [];
-      for (var headeri in gotdata.table.cols) {
-        headers.push(gotdata.table.cols[headeri].label ||
-                     gotdata.table.cols[headeri].id);
+      for (var headeri in rawdata.table.cols) {
+        headers.push(rawdata.table.cols[headeri].label ||
+                     rawdata.table.cols[headeri].id);
       }
       data = [];
-      for (var rowi in gotdata.table.rows) {
-        var row = gotdata.table.rows[rowi];
+      for (var rowi in rawdata.table.rows) {
+        var row = rawdata.table.rows[rowi];
         var orow = [];
         for (var coli in row.c) {
           var col = row.c[coli];
@@ -1063,19 +1078,19 @@
         }
         data.push(orow);
       }
-    } else if (gotdata.data && gotdata.cols) {
+    } else if (rawdata.data && rawdata.cols) {
       // eqldata.com format
       headers = [];
-      for (var coli in gotdata.cols) {
-        var col = gotdata.cols[coli];
+      for (var coli in rawdata.cols) {
+        var col = rawdata.cols[coli];
         headers.push(col.caption);
       }
-      data = gotdata.data;
+      data = rawdata.data;
     } else {
       // assume simple [[cols...]...] (two-dimensional array) format, where
       // the first row is the headers.
-      headers = gotdata.shift();
-      data = gotdata;
+      headers = rawdata[0];
+      data = rawdata.slice(1);
     }
     types = guessTypes(data);
     parseDates(data, types);
@@ -1083,32 +1098,40 @@
   }
 
 
-  var _queue = [];
-
-
-  function enqueue() {
-    _queue.push([].slice.apply(arguments));
+  function enqueue(queue, stepname, func) {
+    queue.push([stepname, func]);
   }
 
 
-  function runqueue(after_each) {
+  function runqueue(queue, ingrid, done, showstatus, wrap_each, after_each) {
     var step = function(i) {
-      if (i < _queue.length) {
-        var el = _queue[i];
-        var text = el[0], func = el[1], args = el.slice(2);
-        showstatus('Running step ' + (+i + 1) + ' of ' + _queue.length + '...',
-                   text);
+      if (i < queue.length) {
+        var el = queue[i];
+        var text = el[0], func = el[1];
+        if (showstatus) {
+          showstatus('Running step ' + (+i + 1) + ' of ' +
+                     queue.length + '...',
+                     text);
+        }
         setTimeout(function() {
           var start = Date.now();
-          wrap(func).apply(null, args);
-          var end = Date.now();
-          if (after_each) {
-            after_each(i + 1, _queue.length, text, end - start);
-          }
-          step(i + 1);
+          var wfunc = wrap_each ? wrap_each(func) : func;
+          wfunc(ingrid, function(outgrid) {
+            var end = Date.now();
+            if (after_each) {
+              after_each(outgrid, i + 1, queue.length, text, end - start);
+            }
+            ingrid = outgrid;
+            step(i + 1);
+          });
         }, 0);
       } else {
-        showstatus('');
+        if (showstatus) {
+          showstatus('');
+        }
+        if (done) {
+          done(ingrid);
+        }
       }
     };
     step(0);
@@ -1122,18 +1145,17 @@
   }
 
 
-  function gotData(args, gotdata) {
-    var grid;
-    enqueue('parse', function() {
-      console.debug('gotdata:', gotdata);
-      grid = gridFromData(gotdata);
-      console.debug('grid:', grid);
-    });
-
+  function addTransforms(queue, args) {
+    var trace = args.get('trace');
     var argi;
+
+    // helper function for synchronous transformations (ie. ones that return
+    // the output grid rather than calling a callback)
     var transform = function(f, arg) {
-      enqueue(args.all[argi][0] + '=' + args.all[argi][1], function() {
-        grid = f(grid, arg);
+      enqueue(queue, args.all[argi][0] + '=' + args.all[argi][1],
+              function(ingrid, done) {
+        var outgrid = f(ingrid, arg);
+        done(outgrid);
       });
     };
 
@@ -1163,12 +1185,16 @@
         transform(doExtractRegexp, argval);
       }
     }
+  }
 
-    var chartops = args.get('chart'), trace = args.get('trace');
+
+  function addRenderers(queue, args) {
+    var trace = args.get('trace');
+    var chartops = args.get('chart');
     var t, datatable;
     var options = {};
 
-    enqueue('gentable', function() {
+    enqueue(queue, 'gentable', function(grid, done) {
       if (chartops) {
         var chartbits = chartops.split(',');
         var charttype = chartbits.shift();
@@ -1236,7 +1262,10 @@
           throw new Error('unknown chart type "' + charttype + '"');
         }
         $(el).height(window.innerHeight);
-        datatable = dataToGvizTable(grid, { show_only_lastseg: true });
+        datatable = dataToGvizTable(grid, {
+            show_only_lastseg: true,
+            bool_to_num: true
+        });
       } else {
         var el = document.getElementById('viztable');
         t = new google.visualization.Table(el);
@@ -1260,15 +1289,27 @@
           datetimeformat.format(datatable, coli);
         }
       }
+      done(grid);
     });
 
-    enqueue(chartops ? 'chart=' + chartops : 'view', function() {
-      t.draw(datatable, options);
+    enqueue(queue, chartops ? 'chart=' + chartops : 'view',
+            function(grid, done) {
+      if (grid.data.length) {
+        t.draw(datatable, options);
+      } else {
+        var el = document.getElementById('vizchart');
+        el.innerHTML = 'Empty dataset.';
+      }
+      done(grid);
     });
+  }
 
+
+  function finishQueue(queue, args, done) {
+    var trace = args.get('trace');
     if (trace) {
       var prevdata;
-      var after_each = function(stepi, nsteps, text, msec_time) {
+      var after_each = function(grid, stepi, nsteps, text, msec_time) {
         $('#vizlog').append('<div class="vizstep" id="step' + stepi + '">' +
                             '  <div class="text"></div>' +
                             '  <div class="grid"></div>' +
@@ -1293,9 +1334,9 @@
           $('.vizstep').show();
         }
       };
-      runqueue(after_each);
+      runqueue(queue, null, done, showstatus, wrap, after_each);
     } else {
-      runqueue();
+      runqueue(queue, null, done, showstatus, wrap);
     }
   }
 
@@ -1311,11 +1352,22 @@
   }
 
 
+  function argsToArray(args) {
+    // call Array's slice() function on an 'arguments' structure, which is
+    // like an array but missing functions like slice().  The result is a
+    // real Array object, which is more useful.
+    return [].slice.apply(args);
+  }
+
+
   function wrap(func) {
-    var pre_args = [].slice.call(arguments, 1);
+    // pre_args is the arguments as passed at wrap() time
+    var pre_args = argsToArray(arguments).slice(1);
     var f = function() {
       try {
-        return func.apply(null, pre_args.concat([].slice.call(arguments)));
+        // post_args is the arguments as passed when calling f()
+        var post_args = argsToArray(arguments);
+        return func.apply(null, pre_args.concat(post_args));
       } catch (e) {
         $('#vizchart').hide();
         $('#viztable').hide();
@@ -1447,23 +1499,58 @@
   }
 
 
-  function _run(query) {
+  function addUrlGetters(queue, args, startdata) {
+    if (!startdata) {
+      var url = args.get('url');
+      console.debug('original data url:', url);
+      if (!url) throw new Error('Missing url= in query parameter');
+      url = extendDataUrl(url);
+      showstatus('Loading <a href="' + encodeURI(url) + '">data</a>...');
+
+      enqueue(queue, 'get data', function(_, done) {
+        getUrlData(url, wrap(done), wrap(gotError, url));
+      });
+    } else {
+      enqueue(queue, 'init data', function(_, done) {
+        done(startdata);
+      });
+    }
+
+    enqueue(queue, 'parse', function(rawdata, done) {
+      console.debug('rawdata:', rawdata);
+      var outgrid = gridFromData(rawdata);
+      console.debug('grid:', outgrid);
+      done(outgrid);
+    });
+  }
+
+
+  function exec(query, startdata, done) {
     var args = parseArgs(query);
-    var url = args.get('url');
-    console.debug('original data url:', url);
-    if (!url) throw new Error('Missing url= in query parameter');
-    url = extendDataUrl(url);
-    showstatus('Loading <a href="' + encodeURI(url) + '">data</a>...');
-    getUrlData(url, wrap(gotData, args), wrap(gotError, url));
+    var queue = [];
+    addUrlGetters(queue, args, startdata);
+    addTransforms(queue, args);
+    runqueue(queue, startdata, done);
+  }
+
+
+  function render(query, startdata, done) {
+    var args = parseArgs(query);
     var editlink = args.get('editlink');
     if (editlink == 0) {
       $('#editmenu').hide();
     }
+
+    var queue = [];
+    addUrlGetters(queue, args, startdata);
+    addTransforms(queue, args);
+    addRenderers(queue, args);
+    finishQueue(queue, args, done);
   }
 
+
   return {
     internal: {
-      parseArgs: parseArgs,
       trySplitOne: trySplitOne,
       dataToGvizTable: dataToGvizTable,
       guessTypes: guessTypes,
@@ -1477,8 +1564,13 @@
       fillNullsWithZero: fillNullsWithZero,
       urlMinusPath: urlMinusPath,
       checkUrlSafety: checkUrlSafety,
+      argsToArray: argsToArray,
+      enqueue: enqueue,
+      runqueue: runqueue,
       gridFromData: gridFromData
     },
-    render: wrap(_run)
+    parseArgs: parseArgs,
+    exec: exec,
+    render: wrap(render)
   };
 })();
diff --git a/setauth.html b/setauth.html
index 776cde7..a45b023 100644
--- a/setauth.html
+++ b/setauth.html
@@ -5,13 +5,13 @@
 <script src="render.js"></script>
 <script>
 
-var args = afterquery.internal.parseArgs(
+var args = afterquery.parseArgs(
     (window.location.search || '?') + '&' + window.location.hash.substr(1));
 
 var token = args.get('access_token') || args.get('auth');
 var state = args.get('state');
 console.debug('state is:', state);
-var stateargs = afterquery.internal.parseArgs('?' + (state || ''));
+var stateargs = afterquery.parseArgs('?' + (state || ''));
 var url = args.get('url') || stateargs.get('url');
 var continue_url = args.get('continue') || stateargs.get('continue');
 var hostpart = url ? afterquery.internal.urlMinusPath(url) : null;
diff --git a/t/trender.js b/t/trender.js
index f8cc05d..1dd9e17 100644
--- a/t/trender.js
+++ b/t/trender.js
@@ -29,17 +29,50 @@
   }
 };
 
+
 wvtest('parseArgs', function() {
   var query = '?a=b&c=d&e==f&&g=h&g=i%25%31%31&a=&a=x';
-  var args = afterquery.internal.parseArgs(query);
+  var args = afterquery.parseArgs(query);
   WVPASSEQ(args.all.join('|'), 'a,b|c,d|e,=f|,|g,h|g,i%11|a,|a,x');
   WVPASSEQ(args.get('a'), 'x');
   WVPASSEQ(args.get(''), '');
   WVPASSEQ(args.get('g'), 'i%11');
 
-  WVPASSEQ(afterquery.internal.parseArgs('').all.join('|'), ',');
-  WVPASSEQ(afterquery.internal.parseArgs('?').all.join('|'), ',');
-  WVPASSEQ(afterquery.internal.parseArgs('abc=def').all.join('|'), 'bc,def');
+  WVPASSEQ(afterquery.parseArgs('').all.join('|'), ',');
+  WVPASSEQ(afterquery.parseArgs('?').all.join('|'), ',');
+  WVPASSEQ(afterquery.parseArgs('abc=def').all.join('|'), 'abc,def');
+});
+
+
+wvtest('argsToArray', function() {
+  var x = function() {
+    return afterquery.internal.argsToArray(arguments);
+  };
+  WVPASSEQ(x(1,2,3), [1,2,3]);
+  WVPASSEQ(x(1,2,3).slice(1), [2,3]);
+});
+
+
+// Fake setTimeout function for testing runqueue().  Just execute the
+// requested function immediately.
+function setTimeout(func, when) {
+  func();
+}
+
+
+wvtest('queue', function() {
+  var queue = [];
+  var vfinal = 'never-assigned';
+  afterquery.internal.enqueue(queue, 'step1', function(v, done) {
+    done(v + '1');
+  });
+  afterquery.internal.enqueue(queue, 'step2', function(v, done) {
+    done(v + '2');
+  });
+  afterquery.internal.runqueue(queue, 'foo', function(v) {
+    vfinal = v;
+  });
+  WVPASSEQ(vfinal, 'foo12');
 });
 
 
@@ -127,3 +160,49 @@
   WVPASSEQ(afterquery.internal.urlMinusPath('//foo/blah//whatever'),
            '//foo');
 });
+
+
+function _gridAsText(grid) {
+  return [].concat(grid.headers, grid.types, grid.data);
+}
+
+
+wvtest('gridFromData', function() {
+  var rawdata = [
+    ['a', 'b', 'c'],
+    [1, 2, 3]
+  ];
+  var otherdata = [
+    ['a', 'b', 'c'],
+    [1, 2, 4]
+  ];
+  var grid = {
+    headers: ['a', 'b', 'c'],
+    data: [[1, 2, 3]],
+    types: ['boolean', 'number', 'number']
+  };
+  var gtext = _gridAsText(grid);
+  WVPASSEQ(_gridAsText(afterquery.internal.gridFromData(grid)), gtext);
+  WVPASSEQ(_gridAsText(afterquery.internal.gridFromData(rawdata)), gtext);
+  WVPASSNE(_gridAsText(afterquery.internal.gridFromData(otherdata)), gtext);
+});
+
+
+wvtest('exec', function() {
+  var rawdata = [
+    ['a', 'b', 'c'],
+    [1, 2, 3],
+    [1, 5, 6],
+    [1, 5, 9]
+  ];
+  afterquery.exec('group=a;', rawdata, function(grid) {
+    WVPASSEQ(grid.data, [[1]]);
+  });
+  afterquery.exec('group=a,b;count(c)&group=a', rawdata, function(grid) {
+    WVPASSEQ(grid.data, [[1, 7, 3]]);
+  });
+  afterquery.exec(['group=a,b;count(c)', 'pivot=a;b;c'], rawdata,
+		  function(grid) {
+    WVPASSEQ(grid.data, [[1, 1, 2]]);
+  });
+});
diff --git a/wvtest.js b/wvtest.js
index 3a1008c..3fb8120 100644
--- a/wvtest.js
+++ b/wvtest.js
@@ -4,16 +4,26 @@
 function lookup(filename, line) {
     var f = files[filename];
     if (!f) {
-	f = files[filename] = read(filename).split('\n');
+        try {
+            f = files[filename] = read(filename).split('\n');
+        } catch (e) {
+            f = files[filename] = [];
+        }
     }
-    return f[line-1]; // file line numbers are 1-based
+    return f[line-1] || 'BAD_LINE'; // file line numbers are 1-based
 }
 
 
+// TODO(apenwarr): Right now this only really works right on chrome.
+// Maybe take some advice from this article:
+//  http://stackoverflow.com/questions/5358983/javascript-stack-inspection-on-safari-mobile-ipad
 function trace() {
     var FILELINE_RE = /[\b\s]\(?([^:\s]+):(\d+)/;
     var out = [];
     var e = Error().stack;
+    if (!e) {
+        return [['UNKNOWN', 0], ['UNKNOWN', 0]];
+    }
     var lines = e.split('\n');
     for (i in lines) {
 	if (i > 2) {