summaryrefslogtreecommitdiff
path: root/public-src
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@lukeshu.com>2018-03-14 18:18:31 -0400
committerLuke Shumaker <lukeshu@lukeshu.com>2018-03-17 13:49:41 -0400
commitb54a1c9686eec3c1114e9b58cb67679ba59c45bd (patch)
tree0bdb2f3ed51ff077a8c3e337e4bc556aacec108e /public-src
parent54feeb027d6e5a760b49769dfe695ea2591dc6fe (diff)
directories
Diffstat (limited to 'public-src')
-rw-r--r--public-src/colordate.js41
-rwxr-xr-xpublic-src/index.html.gen46
l---------public-src/jarmon-dependencies.js1
-rw-r--r--public-src/jarmon-style/jquerytools.tabs.tabs-no-images.scss69
l---------public-src/jarmon-style/loading.gif1
l---------public-src/jarmon-style/next.gif1
l---------public-src/jarmon-style/prev.gif1
-rw-r--r--public-src/jarmon-style/style.scss119
-rw-r--r--public-src/jarmon.html.in29
l---------public-src/jarmon.js1
-rw-r--r--public-src/sorttable.js495
-rw-r--r--public-src/style.scss76
12 files changed, 880 insertions, 0 deletions
diff --git a/public-src/colordate.js b/public-src/colordate.js
new file mode 100644
index 0000000..e5331eb
--- /dev/null
+++ b/public-src/colordate.js
@@ -0,0 +1,41 @@
+(function() {
+ // in milliseconds
+ var now = Date.now();
+ var oneday = 1000*60*60*24;
+
+ // maps from a point on iRange to a point on oRange
+ var mapRange = function(iRange, oRange, iPoint) {
+ var pct = (iPoint - iRange[0])/(iRange[1]-iRange[0]);
+ if (pct < 0) {
+ pct = 0;
+ } else if (pct > 1) {
+ pct = 1;
+ }
+ var oPoint = oRange[0] + (pct * (oRange[1]-oRange[0]));
+ return oPoint;
+ }
+
+
+ var rgb = function(r, g, b) {
+ return "rgb(" + Math.trunc(r) + "," + Math.trunc(g) + "," + Math.trunc(b) + ")";
+ };
+
+ var date2color = function(t) {
+ var max = 0xFF;
+ var red = mapRange([now-oneday, now-(oneday/2)],
+ [max, 0],
+ t);
+ var green = mapRange([now-(oneday/2), now],
+ [0, max],
+ t);
+ return rgb(max-green, max-red, max-green-red);
+ };
+
+ var main = function() {
+ document.querySelectorAll('time.daily').forEach(function(time) {
+ time.style.backgroundColor = date2color(Date.parse(time.dateTime));
+ });
+ };
+
+ document.addEventListener("DOMContentLoaded", main, false);
+})();
diff --git a/public-src/index.html.gen b/public-src/index.html.gen
new file mode 100755
index 0000000..c43beec
--- /dev/null
+++ b/public-src/index.html.gen
@@ -0,0 +1,46 @@
+#!/bin/sh
+set -e
+cd "$(dirname -- "$0")"
+
+col() {
+ echo '<div>'
+ eval "$*"
+ echo '</div>'
+}
+
+echo '<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Dashboard</title>
+ <link rel=stylesheet href=style.css>
+ <script src="sorttable.js"></script>
+ <script src="colordate.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="jarmon-style/style.css" />
+ <link rel="stylesheet" type="text/css" href="jarmon-style/jquerytools.tabs.tabs-no-images.css" />
+ <link rel="stylesheet" type="text/css" href="jarmon-style/jquerytools.dateinput.skin1.css" />
+
+ <!--<script src="jarmon-dependencies.js"></script>-->
+ <script src="./jarmon-dependencies/jquery-1.6.3.js"></script>
+ <script src="./jarmon-dependencies/flot-0.8.3/excanvas.js"></script>
+ <script src="./jarmon-dependencies/flot-0.8.3/jquery.flot.js"></script>
+ <script src="./jarmon-dependencies/flot-0.8.3/jquery.flot.time.js"></script>
+ <script src="./jarmon-dependencies/flot-0.8.3/jquery.flot.stack.js"></script>
+ <script src="./jarmon-dependencies/flot-0.8.3/jquery.flot.selection.js"></script>
+ <script src="./jarmon-dependencies/rrdFile.js"></script>
+ <script src="./jarmon-dependencies/binaryXHR.js"></script>
+ <script src="./jarmon-dependencies/dateinput.js"></script>
+ <script src="./jarmon-dependencies/tabs.js"></script>
+ <script src="./jarmon-dependencies/toolbox.history.js"></script>
+
+ <script src="jarmon.js"></script>
+ <script src="config-jarmon-proton.js"></script>
+ <script src="config-jarmon-winston.js"></script>
+</head>
+<body>'
+col sed 's/@name@/proton/' jarmon.html.in
+col sed 's/@name@/winston/' jarmon.html.in
+col cat tls.html.part crtsh.html.part diff.html.part
+echo '</body>
+</html>'
diff --git a/public-src/jarmon-dependencies.js b/public-src/jarmon-dependencies.js
new file mode 120000
index 0000000..9874026
--- /dev/null
+++ b/public-src/jarmon-dependencies.js
@@ -0,0 +1 @@
+../graph/jarmon-git/docs/examples/assets/js/dependencies.js \ No newline at end of file
diff --git a/public-src/jarmon-style/jquerytools.tabs.tabs-no-images.scss b/public-src/jarmon-style/jquerytools.tabs.tabs-no-images.scss
new file mode 100644
index 0000000..d97f579
--- /dev/null
+++ b/public-src/jarmon-style/jquerytools.tabs.tabs-no-images.scss
@@ -0,0 +1,69 @@
+/* Skin for jQuery Tools tabs.
+ *
+ * Based on <https://github.com/jquerytools/jquerytools.github.com/blob/master/media/css/tabs-no-images.css>.
+ *
+ * Documentation on tabs: <http://jquerytools.github.io/documentation/tabs/index.html>
+ */
+
+$tabs-tab-height: 15px;
+$tabs-tab-vpad: 2px;
+$tabs-tab-hpad: 10px;
+$tabs-border-width: 1px;
+$tabs-border-style: solid #666;
+$tabs-background-primary: #ddd;
+$tabs-background-secondary: #efefef;
+
+/* root element for tab bar */
+ul.css-tabs {
+ margin: 0;
+ padding: 0;
+ height: $tabs-tab-height;
+ border-bottom: $tabs-border-width $tabs-border-style;
+
+ /* single tab */
+ li {
+ float: left;
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+
+ /* link inside the tab */
+ a {
+ display:block;
+ height: $tabs-tab-height; /* $tabs-tab-height - 2*($tabs-tab-vpad+$tabs-border-width) */
+ padding: $tabs-tab-vpad $tabs-tab-hpad;
+ border: $tabs-border-width $tabs-border-style;
+ border-bottom: 0;
+
+ margin-right: 2px;
+ border-radius: 4px 4px 0 0;
+ text-decoration: none;
+
+ background: $tabs-background-secondary;
+ color: #777;
+
+ &:hover {
+ background-color: #F7F7F7;
+ color: #333;
+ }
+
+ /* selected tab */
+ &.current {
+ background: $tabs-background-primary;
+ border-bottom: $tabs-border-width solid $tabs-background-primary;
+ color: #000;
+ cursor:default;
+ }
+ }
+ }
+}
+
+/* tab pane */
+.css-panes > div {
+ display: none;
+ border: $tabs-border-width $tabs-border-style;
+ border-top: 0;
+ background: $tabs-background-primary;
+
+ padding: 15px 20px;
+}
diff --git a/public-src/jarmon-style/loading.gif b/public-src/jarmon-style/loading.gif
new file mode 120000
index 0000000..03b9781
--- /dev/null
+++ b/public-src/jarmon-style/loading.gif
@@ -0,0 +1 @@
+../../graph/jarmon-git/docs/examples/assets/icons/loading.gif \ No newline at end of file
diff --git a/public-src/jarmon-style/next.gif b/public-src/jarmon-style/next.gif
new file mode 120000
index 0000000..16a6d9d
--- /dev/null
+++ b/public-src/jarmon-style/next.gif
@@ -0,0 +1 @@
+../../graph/jarmon-git/docs/examples/assets/icons/next.gif \ No newline at end of file
diff --git a/public-src/jarmon-style/prev.gif b/public-src/jarmon-style/prev.gif
new file mode 120000
index 0000000..72199f2
--- /dev/null
+++ b/public-src/jarmon-style/prev.gif
@@ -0,0 +1 @@
+../../graph/jarmon-git/docs/examples/assets/icons/prev.gif \ No newline at end of file
diff --git a/public-src/jarmon-style/style.scss b/public-src/jarmon-style/style.scss
new file mode 100644
index 0000000..5641550
--- /dev/null
+++ b/public-src/jarmon-style/style.scss
@@ -0,0 +1,119 @@
+.jarmon {
+ border: solid 1px black;
+ border-radius: 4px;
+
+ .chartRangeControl {
+ .range-inputs {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ & > * {
+ width: 20%;
+ margin: 1px;
+ padding: 0;
+ border: solid 1px #666;
+ border-radius: 2px;
+ &[type="button"] {
+ width: auto;
+ }
+ &[type="datetime-local"] {
+ width: 25%;
+ }
+ }
+ }
+ .range-preview {
+ height: 42px;
+ width: 100%;
+ }
+ }
+
+ .css-panes > div {
+ padding: 0;
+ padding-left: 15px;
+ }
+
+ h2 {
+ margin: 0;
+ font-size: 100%;
+ text-align: center;
+ }
+
+ .tabbed-chart-interface {
+ .css-panes {
+ height: 550px;
+ & > div {
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ }
+ }
+ .chart-container {
+ &.loading .title {
+ background-repeat: no-repeat;
+ background-position: 0 50%;
+ background-image: url(loading.gif);
+ }
+
+ .chart {
+ height: 125px;
+ width: 100%;
+ margin: 0 auto 0 auto;
+ clear: both;
+
+ .tickLabel, /* flot 0.7 */
+ .tick-label /* flot 0.8 */
+ {
+ overflow:hidden;
+ }
+
+ .yaxisUnitLabel {
+ /* Y */
+ position: absolute;
+ top: 50%;
+ /* In this translateY, the 75% is: 50% for the `top:50%` overshooting
+ * by our `height/2`; the additional 25% accounts
+ * for 1 line-height being chopped off the bottom
+ * of the graph for the x-axis labels. */
+ transform: translateY(-75%);
+
+ /* X */
+ width: 100px;
+ margin-left: -100px;
+ text-align: right;
+
+ /* styling */
+ padding: 2px;
+
+ /* rotate */
+ transform: rotate(-90deg);
+ width: 0;
+ margin-left: -10px;
+ }
+ }
+
+ .graph-legend {
+ width: 100%;
+ padding: 2px 0;
+ margin: 2px auto 0;
+ background-color: #f7f7f7;
+
+ .legendItem {
+ float: left;
+ cursor: pointer;
+ margin-right: 20px;
+ margin-top: 5px;
+ margin-left: 5px;
+
+ .legendColorBox {
+ float: left;
+ margin-right: 5px;
+ }
+
+ &.disabled {
+ text-decoration: line-through;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/public-src/jarmon.html.in b/public-src/jarmon.html.in
new file mode 100644
index 0000000..3486edb
--- /dev/null
+++ b/public-src/jarmon.html.in
@@ -0,0 +1,29 @@
+<div>
+ <div class="jarmon @name@">
+ <div class="chart-container">
+ <h2 class="title"></h2>
+ <form>
+ <input name="chart_edit" value="Edit" type="button" />
+ <input name="chart_delete" value="Delete" type="button" />
+ </form>
+ <div class="error"></div>
+ <div class="chart"></div>
+ <div class="graph-legend"></div>
+ </div>
+ <div class="chartRangeControl">
+ <form>
+ <div class="range-inputs">
+ <input name="from" type="datetime-local" step="1" />
+ <input name="to" type="datetime-local" step="1" />
+ <select name="shortcuts" title="Time range shortcuts - click to select an alternative time range" ></select>
+ <select name="tzoffset" title="Timezone offset - click to choose a custom timezone offset" ></select>
+ <input name="action" value="Update" type="button"
+ title="Graph update - click to update all graphs" />
+ </div>
+ <div class="range-preview"
+ title="Time range preview - click and drag to select a custom timerange" ></div>
+ </form>
+ </div>
+ <div class="tabbed-chart-interface"></div>
+ </div>
+</div>
diff --git a/public-src/jarmon.js b/public-src/jarmon.js
new file mode 120000
index 0000000..1c0e4a9
--- /dev/null
+++ b/public-src/jarmon.js
@@ -0,0 +1 @@
+../graph/jarmon-git/jarmon/jarmon.js \ No newline at end of file
diff --git a/public-src/sorttable.js b/public-src/sorttable.js
new file mode 100644
index 0000000..38b0fc6
--- /dev/null
+++ b/public-src/sorttable.js
@@ -0,0 +1,495 @@
+/*
+ SortTable
+ version 2
+ 7th April 2007
+ Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
+
+ Instructions:
+ Download this file
+ Add <script src="sorttable.js"></script> to your HTML
+ Add class="sortable" to any table you'd like to make sortable
+ Click on the headers to sort
+
+ Thanks to many, many people for contributions and suggestions.
+ Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
+ This basically means: do what you want with it.
+*/
+
+
+var stIsIE = /*@cc_on!@*/false;
+
+sorttable = {
+ init: function() {
+ // quit if this function has already been called
+ if (arguments.callee.done) return;
+ // flag this function so we don't do the same thing twice
+ arguments.callee.done = true;
+ // kill the timer
+ if (_timer) clearInterval(_timer);
+
+ if (!document.createElement || !document.getElementsByTagName) return;
+
+ sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
+
+ forEach(document.getElementsByTagName('table'), function(table) {
+ if (table.className.search(/\bsortable\b/) != -1) {
+ sorttable.makeSortable(table);
+ }
+ });
+
+ },
+
+ makeSortable: function(table) {
+ if (table.getElementsByTagName('thead').length == 0) {
+ // table doesn't have a tHead. Since it should have, create one and
+ // put the first table row in it.
+ the = document.createElement('thead');
+ the.appendChild(table.rows[0]);
+ table.insertBefore(the,table.firstChild);
+ }
+ // Safari doesn't support table.tHead, sigh
+ if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
+
+ if (table.tHead.rows.length != 1) return; // can't cope with two header rows
+
+ // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
+ // "total" rows, for example). This is B&R, since what you're supposed
+ // to do is put them in a tfoot. So, if there are sortbottom rows,
+ // for backwards compatibility, move them to tfoot (creating it if needed).
+ sortbottomrows = [];
+ for (var i=0; i<table.rows.length; i++) {
+ if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
+ sortbottomrows[sortbottomrows.length] = table.rows[i];
+ }
+ }
+ if (sortbottomrows) {
+ if (table.tFoot == null) {
+ // table doesn't have a tfoot. Create one.
+ tfo = document.createElement('tfoot');
+ table.appendChild(tfo);
+ }
+ for (var i=0; i<sortbottomrows.length; i++) {
+ tfo.appendChild(sortbottomrows[i]);
+ }
+ delete sortbottomrows;
+ }
+
+ // work through each column and calculate its type
+ headrow = table.tHead.rows[0].cells;
+ for (var i=0; i<headrow.length; i++) {
+ // manually override the type with a sorttable_type attribute
+ if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
+ mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
+ if (mtch) { override = mtch[1]; }
+ if (mtch && typeof sorttable["sort_"+override] == 'function') {
+ headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
+ } else {
+ headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
+ }
+ // make it clickable to sort
+ headrow[i].sorttable_columnindex = i;
+ headrow[i].sorttable_tbody = table.tBodies[0];
+ dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) {
+
+ if (this.className.search(/\bsorttable_sorted\b/) != -1) {
+ // if we're already sorted by this column, just
+ // reverse the table, which is quicker
+ sorttable.reverse(this.sorttable_tbody);
+ this.className = this.className.replace('sorttable_sorted',
+ 'sorttable_sorted_reverse');
+ this.removeChild(document.getElementById('sorttable_sortfwdind'));
+ sortrevind = document.createElement('span');
+ sortrevind.id = "sorttable_sortrevind";
+ sortrevind.innerHTML = stIsIE ? '&nbsp<font face="webdings">5</font>' : '&nbsp;&#x25B4;';
+ this.appendChild(sortrevind);
+ return;
+ }
+ if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
+ // if we're already sorted by this column in reverse, just
+ // re-reverse the table, which is quicker
+ sorttable.reverse(this.sorttable_tbody);
+ this.className = this.className.replace('sorttable_sorted_reverse',
+ 'sorttable_sorted');
+ this.removeChild(document.getElementById('sorttable_sortrevind'));
+ sortfwdind = document.createElement('span');
+ sortfwdind.id = "sorttable_sortfwdind";
+ sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
+ this.appendChild(sortfwdind);
+ return;
+ }
+
+ // remove sorttable_sorted classes
+ theadrow = this.parentNode;
+ forEach(theadrow.childNodes, function(cell) {
+ if (cell.nodeType == 1) { // an element
+ cell.className = cell.className.replace('sorttable_sorted_reverse','');
+ cell.className = cell.className.replace('sorttable_sorted','');
+ }
+ });
+ sortfwdind = document.getElementById('sorttable_sortfwdind');
+ if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
+ sortrevind = document.getElementById('sorttable_sortrevind');
+ if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
+
+ this.className += ' sorttable_sorted';
+ sortfwdind = document.createElement('span');
+ sortfwdind.id = "sorttable_sortfwdind";
+ sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
+ this.appendChild(sortfwdind);
+
+ // build an array to sort. This is a Schwartzian transform thing,
+ // i.e., we "decorate" each row with the actual sort key,
+ // sort based on the sort keys, and then put the rows back in order
+ // which is a lot faster because you only do getInnerText once per row
+ row_array = [];
+ col = this.sorttable_columnindex;
+ rows = this.sorttable_tbody.rows;
+ for (var j=0; j<rows.length; j++) {
+ row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
+ }
+ /* If you want a stable sort, uncomment the following line */
+ //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
+ /* and comment out this one */
+ row_array.sort(this.sorttable_sortfunction);
+
+ tb = this.sorttable_tbody;
+ for (var j=0; j<row_array.length; j++) {
+ tb.appendChild(row_array[j][1]);
+ }
+
+ delete row_array;
+ });
+ }
+ }
+ },
+
+ guessType: function(table, column) {
+ // guess the type of a column based on its first non-blank row
+ sortfn = sorttable.sort_alpha;
+ for (var i=0; i<table.tBodies[0].rows.length; i++) {
+ text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
+ if (text != '') {
+ if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) {
+ return sorttable.sort_numeric;
+ }
+ // check for a date: dd/mm/yyyy or dd/mm/yy
+ // can have / or . or - as separator
+ // can be mm/dd as well
+ possdate = text.match(sorttable.DATE_RE)
+ if (possdate) {
+ // looks like a date
+ first = parseInt(possdate[1]);
+ second = parseInt(possdate[2]);
+ if (first > 12) {
+ // definitely dd/mm
+ return sorttable.sort_ddmm;
+ } else if (second > 12) {
+ return sorttable.sort_mmdd;
+ } else {
+ // looks like a date, but we can't tell which, so assume
+ // that it's dd/mm (English imperialism!) and keep looking
+ sortfn = sorttable.sort_ddmm;
+ }
+ }
+ }
+ }
+ return sortfn;
+ },
+
+ getInnerText: function(node) {
+ // gets the text we want to use for sorting for a cell.
+ // strips leading and trailing whitespace.
+ // this is *not* a generic getInnerText function; it's special to sorttable.
+ // for example, you can override the cell text with a customkey attribute.
+ // it also gets .value for <input> fields.
+
+ if (!node) return "";
+
+ hasInputs = (typeof node.getElementsByTagName == 'function') &&
+ node.getElementsByTagName('input').length;
+
+ if (node.getAttribute("sorttable_customkey") != null) {
+ return node.getAttribute("sorttable_customkey");
+ }
+ else if (typeof node.textContent != 'undefined' && !hasInputs) {
+ return node.textContent.replace(/^\s+|\s+$/g, '');
+ }
+ else if (typeof node.innerText != 'undefined' && !hasInputs) {
+ return node.innerText.replace(/^\s+|\s+$/g, '');
+ }
+ else if (typeof node.text != 'undefined' && !hasInputs) {
+ return node.text.replace(/^\s+|\s+$/g, '');
+ }
+ else {
+ switch (node.nodeType) {
+ case 3:
+ if (node.nodeName.toLowerCase() == 'input') {
+ return node.value.replace(/^\s+|\s+$/g, '');
+ }
+ case 4:
+ return node.nodeValue.replace(/^\s+|\s+$/g, '');
+ break;
+ case 1:
+ case 11:
+ var innerText = '';
+ for (var i = 0; i < node.childNodes.length; i++) {
+ innerText += sorttable.getInnerText(node.childNodes[i]);
+ }
+ return innerText.replace(/^\s+|\s+$/g, '');
+ break;
+ default:
+ return '';
+ }
+ }
+ },
+
+ reverse: function(tbody) {
+ // reverse the rows in a tbody
+ newrows = [];
+ for (var i=0; i<tbody.rows.length; i++) {
+ newrows[newrows.length] = tbody.rows[i];
+ }
+ for (var i=newrows.length-1; i>=0; i--) {
+ tbody.appendChild(newrows[i]);
+ }
+ delete newrows;
+ },
+
+ /* sort functions
+ each sort function takes two parameters, a and b
+ you are comparing a[0] and b[0] */
+ sort_numeric: function(a,b) {
+ aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
+ if (isNaN(aa)) aa = 0;
+ bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
+ if (isNaN(bb)) bb = 0;
+ return aa-bb;
+ },
+ sort_alpha: function(a,b) {
+ if (a[0]==b[0]) return 0;
+ if (a[0]<b[0]) return -1;
+ return 1;
+ },
+ sort_ddmm: function(a,b) {
+ mtch = a[0].match(sorttable.DATE_RE);
+ y = mtch[3]; m = mtch[2]; d = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt1 = y+m+d;
+ mtch = b[0].match(sorttable.DATE_RE);
+ y = mtch[3]; m = mtch[2]; d = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt2 = y+m+d;
+ if (dt1==dt2) return 0;
+ if (dt1<dt2) return -1;
+ return 1;
+ },
+ sort_mmdd: function(a,b) {
+ mtch = a[0].match(sorttable.DATE_RE);
+ y = mtch[3]; d = mtch[2]; m = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt1 = y+m+d;
+ mtch = b[0].match(sorttable.DATE_RE);
+ y = mtch[3]; d = mtch[2]; m = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt2 = y+m+d;
+ if (dt1==dt2) return 0;
+ if (dt1<dt2) return -1;
+ return 1;
+ },
+
+ shaker_sort: function(list, comp_func) {
+ // A stable sort function to allow multi-level sorting of data
+ // see: http://en.wikipedia.org/wiki/Cocktail_sort
+ // thanks to Joseph Nahmias
+ var b = 0;
+ var t = list.length - 1;
+ var swap = true;
+
+ while(swap) {
+ swap = false;
+ for(var i = b; i < t; ++i) {
+ if ( comp_func(list[i], list[i+1]) > 0 ) {
+ var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
+ swap = true;
+ }
+ } // for
+ t--;
+
+ if (!swap) break;
+
+ for(var i = t; i > b; --i) {
+ if ( comp_func(list[i], list[i-1]) < 0 ) {
+ var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
+ swap = true;
+ }
+ } // for
+ b++;
+
+ } // while(swap)
+ }
+}
+
+/* ******************************************************************
+ Supporting functions: bundled here to avoid depending on a library
+ ****************************************************************** */
+
+// Dean Edwards/Matthias Miller/John Resig
+
+/* for Mozilla/Opera9 */
+if (document.addEventListener) {
+ document.addEventListener("DOMContentLoaded", sorttable.init, false);
+}
+
+/* for Internet Explorer */
+/*@cc_on @*/
+/*@if (@_win32)
+ document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
+ var script = document.getElementById("__ie_onload");
+ script.onreadystatechange = function() {
+ if (this.readyState == "complete") {
+ sorttable.init(); // call the onload handler
+ }
+ };
+/*@end @*/
+
+/* for Safari */
+if (/WebKit/i.test(navigator.userAgent)) { // sniff
+ var _timer = setInterval(function() {
+ if (/loaded|complete/.test(document.readyState)) {
+ sorttable.init(); // call the onload handler
+ }
+ }, 10);
+}
+
+/* for other browsers */
+window.onload = sorttable.init;
+
+// written by Dean Edwards, 2005
+// with input from Tino Zijdel, Matthias Miller, Diego Perini
+
+// http://dean.edwards.name/weblog/2005/10/add-event/
+
+function dean_addEvent(element, type, handler) {
+ if (element.addEventListener) {
+ element.addEventListener(type, handler, false);
+ } else {
+ // assign each event handler a unique ID
+ if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
+ // create a hash table of event types for the element
+ if (!element.events) element.events = {};
+ // create a hash table of event handlers for each element/event pair
+ var handlers = element.events[type];
+ if (!handlers) {
+ handlers = element.events[type] = {};
+ // store the existing event handler (if there is one)
+ if (element["on" + type]) {
+ handlers[0] = element["on" + type];
+ }
+ }
+ // store the event handler in the hash table
+ handlers[handler.$$guid] = handler;
+ // assign a global event handler to do all the work
+ element["on" + type] = handleEvent;
+ }
+};
+// a counter used to create unique IDs
+dean_addEvent.guid = 1;
+
+function removeEvent(element, type, handler) {
+ if (element.removeEventListener) {
+ element.removeEventListener(type, handler, false);
+ } else {
+ // delete the event handler from the hash table
+ if (element.events && element.events[type]) {
+ delete element.events[type][handler.$$guid];
+ }
+ }
+};
+
+function handleEvent(event) {
+ var returnValue = true;
+ // grab the event object (IE uses a global event object)
+ event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
+ // get a reference to the hash table of event handlers
+ var handlers = this.events[event.type];
+ // execute each event handler
+ for (var i in handlers) {
+ this.$$handleEvent = handlers[i];
+ if (this.$$handleEvent(event) === false) {
+ returnValue = false;
+ }
+ }
+ return returnValue;
+};
+
+function fixEvent(event) {
+ // add W3C standard event methods
+ event.preventDefault = fixEvent.preventDefault;
+ event.stopPropagation = fixEvent.stopPropagation;
+ return event;
+};
+fixEvent.preventDefault = function() {
+ this.returnValue = false;
+};
+fixEvent.stopPropagation = function() {
+ this.cancelBubble = true;
+}
+
+// Dean's forEach: http://dean.edwards.name/base/forEach.js
+/*
+ forEach, version 1.0
+ Copyright 2006, Dean Edwards
+ License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+// array-like enumeration
+if (!Array.forEach) { // mozilla already supports this
+ Array.forEach = function(array, block, context) {
+ for (var i = 0; i < array.length; i++) {
+ block.call(context, array[i], i, array);
+ }
+ };
+}
+
+// generic enumeration
+Function.prototype.forEach = function(object, block, context) {
+ for (var key in object) {
+ if (typeof this.prototype[key] == "undefined") {
+ block.call(context, object[key], key, object);
+ }
+ }
+};
+
+// character enumeration
+String.forEach = function(string, block, context) {
+ Array.forEach(string.split(""), function(chr, index) {
+ block.call(context, chr, index, string);
+ });
+};
+
+// globally resolve forEach enumeration
+var forEach = function(object, block, context) {
+ if (object) {
+ var resolve = Object; // default
+ if (object instanceof Function) {
+ // functions have a "length" property
+ resolve = Function;
+ } else if (object.forEach instanceof Function) {
+ // the object implements a custom forEach method so use that
+ object.forEach(block, context);
+ return;
+ } else if (typeof object == "string") {
+ // the object is a string
+ resolve = String;
+ } else if (typeof object.length == "number") {
+ // the object is array-like
+ resolve = Array;
+ }
+ resolve.forEach(object, block, context);
+ }
+};
+
diff --git a/public-src/style.scss b/public-src/style.scss
new file mode 100644
index 0000000..70747bd
--- /dev/null
+++ b/public-src/style.scss
@@ -0,0 +1,76 @@
+/* page layout */
+html {
+ height: 100%;
+}
+body {
+ font-size: 8px;
+ font-family: monospace;
+ height: 100%;
+ margin: 0;
+
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ & > * {
+ flex-direction: column;
+ align-items: center;
+ margin: auto;
+ & > * {
+ margin: auto;
+ }
+ }
+}
+* {
+ box-sizing: border-box;
+}
+/* diff styling */
+table.diff {
+ border-collapse: collapse;
+ td, th {
+ padding: 0;
+ white-space: nowrap;
+ }
+ .diff-del, .diff-del a { color: red; }
+ .diff-add, .diff-add a { color: green; }
+ .diff-dat, .diff-dat a { color: blue; }
+ .diff-ctx, .diff-ctx a { color: black; }
+}
+/* sortable styling */
+table.sortable {
+ border-collapse: collapse;
+ caption p {
+ margin: 0;
+ }
+ td, th {
+ padding: 0;
+ border: solid 1px black;
+ white-space: nowrap;
+ }
+ td {
+ background-color: #F3F3F3;
+ }
+}
+/* generic table interaction */
+table {
+ tr.invalid td {
+ background-color: #F30000 !important;
+ }
+ tr:hover a, tr.invalid:hover a {
+ background-color: #AAAAF3;
+ }
+ td a {
+ text-decoration: none;
+ padding: 0.1em 0.25em;
+ display: block;
+ width: 100%;
+ height: 100%;
+ color: black;
+ }
+}
+/* generic time formatting - for datetimes that we want to render as
+ just a date, but still want to be sortable, we a construct like
+ `<time>DATE<span class=time>TIME</span></time>`, then use this to
+ hide the TIME part of it. */
+time .time {
+ display: none
+}