plutopluto

git clone https://git.ce9e.org/plutopluto.git

commit
96d8af4e0a1b7b1aca33f9da8ec3e5d0da82a5b1
parent
c5cc3d69c33624ef0bafb7c632227aadaa532e7d
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2023-07-01 19:19
refactor js

Diffstat

M plutopluto/index.html 31 ++++++++++++++-----------------
D plutopluto/static/jqlite.js 63 ------------------------------------------------------------
D plutopluto/static/mustache.js 578 ------------------------------------------------------------
M plutopluto/static/plutopluto.js 219 +++++++++++++++++++++++++++++++------------------------------

4 files changed, 126 insertions, 765 deletions


diff --git a/plutopluto/index.html b/plutopluto/index.html

@@ -6,27 +6,24 @@
    6     6 	<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *">
    7     7 	<title>plutopluto</title>
    8     8 	<link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico"/>
    9    -1 	<script src="/static/jqlite.js" type="text/javascript" ></script>
   10    -1 	<script src="/static/mustache.js" type="text/javascript" ></script>
   11    -1 	<script src="/static/plutopluto.js" type="text/javascript" ></script>
   12     9 	<link rel="stylesheet" href="/static/style.css" type="text/css" />
   13    10 </head>
   14    11 
   15    12 <body>
   16    -1 	<ul id="stream" class="posts">
   17    -1 		<li class="post">
   18    -1 			<article>
   19    -1 				<header>
   20    -1 					<a class="source u-bold" href="{{link}}" title="{{title}}">{{source}}</a>
   21    -1 					<span class="meta">
   22    -1 						&middot; Posted on <a href="{{link}}">{{#formatDate}}{{dt}}{{/formatDate}}</a>
   23    -1 					</span>
   24    -1 				</header>
   25    -1 				{{{content}}}
   26    -1 			</article>
   27    -1 		</li>
   28    -1 	</ul>
   29    -1 	<span class="loading" title="click here if nothing happens">loading...</span>
   -1    13 	<template>
   -1    14 		<article>
   -1    15 			<header>
   -1    16 				<a class="source u-bold" href="{{link}}" title="{{title}}">{{source}}</a>
   -1    17 				<span class="meta">
   -1    18 					&middot; Posted on <a href="{{link}}">{{dt}}</a>
   -1    19 				</span>
   -1    20 			</header>
   -1    21 			{{{content}}}
   -1    22 		</article>
   -1    23 	</template>
   -1    24 	<ul id="stream" class="posts"></ul>
   -1    25 	<button type="button" class="loading" title="click here if nothing happens">loading…</button>
   -1    26 	<script src="/static/plutopluto.js" type="module"></script>
   30    27 </body>
   31    28 
   32    29 </html>

diff --git a/plutopluto/static/jqlite.js b/plutopluto/static/jqlite.js

@@ -1,63 +0,0 @@
    1    -1 "use strict";
    2    -1 
    3    -1 var $ = function(query) {
    4    -1 	var ob = {};
    5    -1 
    6    -1 	if (query == document || query.ELEMENT_NODE) {
    7    -1 		ob.target = query;
    8    -1 	} else if (query.trim()[0] === '<') {
    9    -1 		var div = document.createElement('div');
   10    -1 		div.innerHTML = query;
   11    -1 		ob.target = div.children[0];
   12    -1 	} else {
   13    -1 		ob.target = document.querySelector(query);
   14    -1 	}
   15    -1 
   16    -1 	ob.on = function(ev, handler) {
   17    -1 		ob.target.addEventListener(ev, handler);
   18    -1 	};
   19    -1 	ob.ready = function(handler) {
   20    -1 		ob.target.addEventListener('DOMContentLoaded', handler);
   21    -1 	};
   22    -1 	ob.click = function(handler) {
   23    -1 		ob.target.addEventListener('click', handler);
   24    -1 	};
   25    -1 	ob.html = function(value) {
   26    -1 		if (typeof value !== 'undefined') {
   27    -1 			ob.target.innerHTML = value;
   28    -1 		} else {
   29    -1 			return ob.target.innerHTML;
   30    -1 		}
   31    -1 	};
   32    -1 	ob.children = function() {
   33    -1 		return ob.target.children;
   34    -1 	};
   35    -1 	ob.append = function(element) {
   36    -1 		ob.target.appendChild(element.target);
   37    -1 	};
   38    -1 	ob.replace = function(element) {
   39    -1 		ob.target.parentNode.replaceChild(element.target, ob.target);
   40    -1 	}
   41    -1 
   42    -1 	return ob;
   43    -1 };
   44    -1 
   45    -1 $.ajax = function(url, settings) {
   46    -1 	if (settings.hasOwnProperty('data')) {
   47    -1 		var pairs = [];
   48    -1 		for (var key in settings.data) {
   49    -1 			var value = settings.data[key];
   50    -1 			pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
   51    -1 		}
   52    -1 		url += '?' + pairs.join('&');
   53    -1 	}
   54    -1 
   55    -1 	var request = new XMLHttpRequest();
   56    -1 	request.open('GET', url, true);
   57    -1 	request.onload = function() {
   58    -1 		if (this.status >= 199 && this.status < 400){
   59    -1 			settings.success(JSON.parse(this.response));
   60    -1 		}
   61    -1 	};
   62    -1 	request.send();
   63    -1 };

diff --git a/plutopluto/static/mustache.js b/plutopluto/static/mustache.js

@@ -1,578 +0,0 @@
    1    -1 /*!
    2    -1  * mustache.js - Logic-less {{mustache}} templates with JavaScript
    3    -1  * http://github.com/janl/mustache.js
    4    -1  */
    5    -1 
    6    -1 /*global define: false*/
    7    -1 
    8    -1 (function (global, factory) {
    9    -1   if (typeof exports === "object" && exports) {
   10    -1     factory(exports); // CommonJS
   11    -1   } else if (typeof define === "function" && define.amd) {
   12    -1     define(['exports'], factory); // AMD
   13    -1   } else {
   14    -1     factory(global.Mustache = {}); // <script>
   15    -1   }
   16    -1 }(this, function (mustache) {
   17    -1 
   18    -1   var Object_toString = Object.prototype.toString;
   19    -1   var isArray = Array.isArray || function (object) {
   20    -1     return Object_toString.call(object) === '[object Array]';
   21    -1   };
   22    -1 
   23    -1   function isFunction(object) {
   24    -1     return typeof object === 'function';
   25    -1   }
   26    -1 
   27    -1   function escapeRegExp(string) {
   28    -1     return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
   29    -1   }
   30    -1 
   31    -1   // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
   32    -1   // See https://github.com/janl/mustache.js/issues/189
   33    -1   var RegExp_test = RegExp.prototype.test;
   34    -1   function testRegExp(re, string) {
   35    -1     return RegExp_test.call(re, string);
   36    -1   }
   37    -1 
   38    -1   var nonSpaceRe = /\S/;
   39    -1   function isWhitespace(string) {
   40    -1     return !testRegExp(nonSpaceRe, string);
   41    -1   }
   42    -1 
   43    -1   var entityMap = {
   44    -1     "&": "&amp;",
   45    -1     "<": "&lt;",
   46    -1     ">": "&gt;",
   47    -1     '"': '&quot;',
   48    -1     "'": '&#39;',
   49    -1     "/": '&#x2F;'
   50    -1   };
   51    -1 
   52    -1   function escapeHtml(string) {
   53    -1     return String(string).replace(/[&<>"'\/]/g, function (s) {
   54    -1       return entityMap[s];
   55    -1     });
   56    -1   }
   57    -1 
   58    -1   var whiteRe = /\s*/;
   59    -1   var spaceRe = /\s+/;
   60    -1   var equalsRe = /\s*=/;
   61    -1   var curlyRe = /\s*\}/;
   62    -1   var tagRe = /#|\^|\/|>|\{|&|=|!/;
   63    -1 
   64    -1   /**
   65    -1    * Breaks up the given `template` string into a tree of tokens. If the `tags`
   66    -1    * argument is given here it must be an array with two string values: the
   67    -1    * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
   68    -1    * course, the default is to use mustaches (i.e. mustache.tags).
   69    -1    *
   70    -1    * A token is an array with at least 4 elements. The first element is the
   71    -1    * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
   72    -1    * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
   73    -1    * all text that appears outside a symbol this element is "text".
   74    -1    *
   75    -1    * The second element of a token is its "value". For mustache tags this is
   76    -1    * whatever else was inside the tag besides the opening symbol. For text tokens
   77    -1    * this is the text itself.
   78    -1    *
   79    -1    * The third and fourth elements of the token are the start and end indices,
   80    -1    * respectively, of the token in the original template.
   81    -1    *
   82    -1    * Tokens that are the root node of a subtree contain two more elements: 1) an
   83    -1    * array of tokens in the subtree and 2) the index in the original template at
   84    -1    * which the closing tag for that section begins.
   85    -1    */
   86    -1   function parseTemplate(template, tags) {
   87    -1     if (!template)
   88    -1       return [];
   89    -1 
   90    -1     var sections = [];     // Stack to hold section tokens
   91    -1     var tokens = [];       // Buffer to hold the tokens
   92    -1     var spaces = [];       // Indices of whitespace tokens on the current line
   93    -1     var hasTag = false;    // Is there a {{tag}} on the current line?
   94    -1     var nonSpace = false;  // Is there a non-space char on the current line?
   95    -1 
   96    -1     // Strips all whitespace tokens array for the current line
   97    -1     // if there was a {{#tag}} on it and otherwise only space.
   98    -1     function stripSpace() {
   99    -1       if (hasTag && !nonSpace) {
  100    -1         while (spaces.length)
  101    -1           delete tokens[spaces.pop()];
  102    -1       } else {
  103    -1         spaces = [];
  104    -1       }
  105    -1 
  106    -1       hasTag = false;
  107    -1       nonSpace = false;
  108    -1     }
  109    -1 
  110    -1     var openingTagRe, closingTagRe, closingCurlyRe;
  111    -1     function compileTags(tags) {
  112    -1       if (typeof tags === 'string')
  113    -1         tags = tags.split(spaceRe, 2);
  114    -1 
  115    -1       if (!isArray(tags) || tags.length !== 2)
  116    -1         throw new Error('Invalid tags: ' + tags);
  117    -1 
  118    -1       openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
  119    -1       closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
  120    -1       closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
  121    -1     }
  122    -1 
  123    -1     compileTags(tags || mustache.tags);
  124    -1 
  125    -1     var scanner = new Scanner(template);
  126    -1 
  127    -1     var start, type, value, chr, token, openSection;
  128    -1     while (!scanner.eos()) {
  129    -1       start = scanner.pos;
  130    -1 
  131    -1       // Match any text between tags.
  132    -1       value = scanner.scanUntil(openingTagRe);
  133    -1 
  134    -1       if (value) {
  135    -1         for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
  136    -1           chr = value.charAt(i);
  137    -1 
  138    -1           if (isWhitespace(chr)) {
  139    -1             spaces.push(tokens.length);
  140    -1           } else {
  141    -1             nonSpace = true;
  142    -1           }
  143    -1 
  144    -1           tokens.push([ 'text', chr, start, start + 1 ]);
  145    -1           start += 1;
  146    -1 
  147    -1           // Check for whitespace on the current line.
  148    -1           if (chr === '\n')
  149    -1             stripSpace();
  150    -1         }
  151    -1       }
  152    -1 
  153    -1       // Match the opening tag.
  154    -1       if (!scanner.scan(openingTagRe))
  155    -1         break;
  156    -1 
  157    -1       hasTag = true;
  158    -1 
  159    -1       // Get the tag type.
  160    -1       type = scanner.scan(tagRe) || 'name';
  161    -1       scanner.scan(whiteRe);
  162    -1 
  163    -1       // Get the tag value.
  164    -1       if (type === '=') {
  165    -1         value = scanner.scanUntil(equalsRe);
  166    -1         scanner.scan(equalsRe);
  167    -1         scanner.scanUntil(closingTagRe);
  168    -1       } else if (type === '{') {
  169    -1         value = scanner.scanUntil(closingCurlyRe);
  170    -1         scanner.scan(curlyRe);
  171    -1         scanner.scanUntil(closingTagRe);
  172    -1         type = '&';
  173    -1       } else {
  174    -1         value = scanner.scanUntil(closingTagRe);
  175    -1       }
  176    -1 
  177    -1       // Match the closing tag.
  178    -1       if (!scanner.scan(closingTagRe))
  179    -1         throw new Error('Unclosed tag at ' + scanner.pos);
  180    -1 
  181    -1       token = [ type, value, start, scanner.pos ];
  182    -1       tokens.push(token);
  183    -1 
  184    -1       if (type === '#' || type === '^') {
  185    -1         sections.push(token);
  186    -1       } else if (type === '/') {
  187    -1         // Check section nesting.
  188    -1         openSection = sections.pop();
  189    -1 
  190    -1         if (!openSection)
  191    -1           throw new Error('Unopened section "' + value + '" at ' + start);
  192    -1 
  193    -1         if (openSection[1] !== value)
  194    -1           throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
  195    -1       } else if (type === 'name' || type === '{' || type === '&') {
  196    -1         nonSpace = true;
  197    -1       } else if (type === '=') {
  198    -1         // Set the tags for the next time around.
  199    -1         compileTags(value);
  200    -1       }
  201    -1     }
  202    -1 
  203    -1     // Make sure there are no open sections when we're done.
  204    -1     openSection = sections.pop();
  205    -1 
  206    -1     if (openSection)
  207    -1       throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
  208    -1 
  209    -1     return nestTokens(squashTokens(tokens));
  210    -1   }
  211    -1 
  212    -1   /**
  213    -1    * Combines the values of consecutive text tokens in the given `tokens` array
  214    -1    * to a single token.
  215    -1    */
  216    -1   function squashTokens(tokens) {
  217    -1     var squashedTokens = [];
  218    -1 
  219    -1     var token, lastToken;
  220    -1     for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  221    -1       token = tokens[i];
  222    -1 
  223    -1       if (token) {
  224    -1         if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
  225    -1           lastToken[1] += token[1];
  226    -1           lastToken[3] = token[3];
  227    -1         } else {
  228    -1           squashedTokens.push(token);
  229    -1           lastToken = token;
  230    -1         }
  231    -1       }
  232    -1     }
  233    -1 
  234    -1     return squashedTokens;
  235    -1   }
  236    -1 
  237    -1   /**
  238    -1    * Forms the given array of `tokens` into a nested tree structure where
  239    -1    * tokens that represent a section have two additional items: 1) an array of
  240    -1    * all tokens that appear in that section and 2) the index in the original
  241    -1    * template that represents the end of that section.
  242    -1    */
  243    -1   function nestTokens(tokens) {
  244    -1     var nestedTokens = [];
  245    -1     var collector = nestedTokens;
  246    -1     var sections = [];
  247    -1 
  248    -1     var token, section;
  249    -1     for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  250    -1       token = tokens[i];
  251    -1 
  252    -1       switch (token[0]) {
  253    -1       case '#':
  254    -1       case '^':
  255    -1         collector.push(token);
  256    -1         sections.push(token);
  257    -1         collector = token[4] = [];
  258    -1         break;
  259    -1       case '/':
  260    -1         section = sections.pop();
  261    -1         section[5] = token[2];
  262    -1         collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
  263    -1         break;
  264    -1       default:
  265    -1         collector.push(token);
  266    -1       }
  267    -1     }
  268    -1 
  269    -1     return nestedTokens;
  270    -1   }
  271    -1 
  272    -1   /**
  273    -1    * A simple string scanner that is used by the template parser to find
  274    -1    * tokens in template strings.
  275    -1    */
  276    -1   function Scanner(string) {
  277    -1     this.string = string;
  278    -1     this.tail = string;
  279    -1     this.pos = 0;
  280    -1   }
  281    -1 
  282    -1   /**
  283    -1    * Returns `true` if the tail is empty (end of string).
  284    -1    */
  285    -1   Scanner.prototype.eos = function () {
  286    -1     return this.tail === "";
  287    -1   };
  288    -1 
  289    -1   /**
  290    -1    * Tries to match the given regular expression at the current position.
  291    -1    * Returns the matched text if it can match, the empty string otherwise.
  292    -1    */
  293    -1   Scanner.prototype.scan = function (re) {
  294    -1     var match = this.tail.match(re);
  295    -1 
  296    -1     if (!match || match.index !== 0)
  297    -1       return '';
  298    -1 
  299    -1     var string = match[0];
  300    -1 
  301    -1     this.tail = this.tail.substring(string.length);
  302    -1     this.pos += string.length;
  303    -1 
  304    -1     return string;
  305    -1   };
  306    -1 
  307    -1   /**
  308    -1    * Skips all text until the given regular expression can be matched. Returns
  309    -1    * the skipped string, which is the entire tail if no match can be made.
  310    -1    */
  311    -1   Scanner.prototype.scanUntil = function (re) {
  312    -1     var index = this.tail.search(re), match;
  313    -1 
  314    -1     switch (index) {
  315    -1     case -1:
  316    -1       match = this.tail;
  317    -1       this.tail = "";
  318    -1       break;
  319    -1     case 0:
  320    -1       match = "";
  321    -1       break;
  322    -1     default:
  323    -1       match = this.tail.substring(0, index);
  324    -1       this.tail = this.tail.substring(index);
  325    -1     }
  326    -1 
  327    -1     this.pos += match.length;
  328    -1 
  329    -1     return match;
  330    -1   };
  331    -1 
  332    -1   /**
  333    -1    * Represents a rendering context by wrapping a view object and
  334    -1    * maintaining a reference to the parent context.
  335    -1    */
  336    -1   function Context(view, parentContext) {
  337    -1     this.view = view == null ? {} : view;
  338    -1     this.cache = { '.': this.view };
  339    -1     this.parent = parentContext;
  340    -1   }
  341    -1 
  342    -1   /**
  343    -1    * Creates a new context using the given view with this context
  344    -1    * as the parent.
  345    -1    */
  346    -1   Context.prototype.push = function (view) {
  347    -1     return new Context(view, this);
  348    -1   };
  349    -1 
  350    -1   /**
  351    -1    * Returns the value of the given name in this context, traversing
  352    -1    * up the context hierarchy if the value is absent in this context's view.
  353    -1    */
  354    -1   Context.prototype.lookup = function (name) {
  355    -1     var cache = this.cache;
  356    -1 
  357    -1     var value;
  358    -1     if (name in cache) {
  359    -1       value = cache[name];
  360    -1     } else {
  361    -1       var context = this, names, index;
  362    -1 
  363    -1       while (context) {
  364    -1         if (name.indexOf('.') > 0) {
  365    -1           value = context.view;
  366    -1           names = name.split('.');
  367    -1           index = 0;
  368    -1 
  369    -1           while (value != null && index < names.length)
  370    -1             value = value[names[index++]];
  371    -1         } else {
  372    -1           value = context.view[name];
  373    -1         }
  374    -1 
  375    -1         if (value != null)
  376    -1           break;
  377    -1 
  378    -1         context = context.parent;
  379    -1       }
  380    -1 
  381    -1       cache[name] = value;
  382    -1     }
  383    -1 
  384    -1     if (isFunction(value))
  385    -1       value = value.call(this.view);
  386    -1 
  387    -1     return value;
  388    -1   };
  389    -1 
  390    -1   /**
  391    -1    * A Writer knows how to take a stream of tokens and render them to a
  392    -1    * string, given a context. It also maintains a cache of templates to
  393    -1    * avoid the need to parse the same template twice.
  394    -1    */
  395    -1   function Writer() {
  396    -1     this.cache = {};
  397    -1   }
  398    -1 
  399    -1   /**
  400    -1    * Clears all cached templates in this writer.
  401    -1    */
  402    -1   Writer.prototype.clearCache = function () {
  403    -1     this.cache = {};
  404    -1   };
  405    -1 
  406    -1   /**
  407    -1    * Parses and caches the given `template` and returns the array of tokens
  408    -1    * that is generated from the parse.
  409    -1    */
  410    -1   Writer.prototype.parse = function (template, tags) {
  411    -1     var cache = this.cache;
  412    -1     var tokens = cache[template];
  413    -1 
  414    -1     if (tokens == null)
  415    -1       tokens = cache[template] = parseTemplate(template, tags);
  416    -1 
  417    -1     return tokens;
  418    -1   };
  419    -1 
  420    -1   /**
  421    -1    * High-level method that is used to render the given `template` with
  422    -1    * the given `view`.
  423    -1    *
  424    -1    * The optional `partials` argument may be an object that contains the
  425    -1    * names and templates of partials that are used in the template. It may
  426    -1    * also be a function that is used to load partial templates on the fly
  427    -1    * that takes a single argument: the name of the partial.
  428    -1    */
  429    -1   Writer.prototype.render = function (template, view, partials) {
  430    -1     var tokens = this.parse(template);
  431    -1     var context = (view instanceof Context) ? view : new Context(view);
  432    -1     return this.renderTokens(tokens, context, partials, template);
  433    -1   };
  434    -1 
  435    -1   /**
  436    -1    * Low-level method that renders the given array of `tokens` using
  437    -1    * the given `context` and `partials`.
  438    -1    *
  439    -1    * Note: The `originalTemplate` is only ever used to extract the portion
  440    -1    * of the original template that was contained in a higher-order section.
  441    -1    * If the template doesn't use higher-order sections, this argument may
  442    -1    * be omitted.
  443    -1    */
  444    -1   Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
  445    -1     var buffer = '';
  446    -1 
  447    -1     // This function is used to render an arbitrary template
  448    -1     // in the current context by higher-order sections.
  449    -1     var self = this;
  450    -1     function subRender(template) {
  451    -1       return self.render(template, context, partials);
  452    -1     }
  453    -1 
  454    -1     var token, value;
  455    -1     for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  456    -1       token = tokens[i];
  457    -1 
  458    -1       switch (token[0]) {
  459    -1       case '#':
  460    -1         value = context.lookup(token[1]);
  461    -1 
  462    -1         if (!value)
  463    -1           continue;
  464    -1 
  465    -1         if (isArray(value)) {
  466    -1           for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
  467    -1             buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
  468    -1           }
  469    -1         } else if (typeof value === 'object' || typeof value === 'string') {
  470    -1           buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
  471    -1         } else if (isFunction(value)) {
  472    -1           if (typeof originalTemplate !== 'string')
  473    -1             throw new Error('Cannot use higher-order sections without the original template');
  474    -1 
  475    -1           // Extract the portion of the original template that the section contains.
  476    -1           value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
  477    -1 
  478    -1           if (value != null)
  479    -1             buffer += value;
  480    -1         } else {
  481    -1           buffer += this.renderTokens(token[4], context, partials, originalTemplate);
  482    -1         }
  483    -1 
  484    -1         break;
  485    -1       case '^':
  486    -1         value = context.lookup(token[1]);
  487    -1 
  488    -1         // Use JavaScript's definition of falsy. Include empty arrays.
  489    -1         // See https://github.com/janl/mustache.js/issues/186
  490    -1         if (!value || (isArray(value) && value.length === 0))
  491    -1           buffer += this.renderTokens(token[4], context, partials, originalTemplate);
  492    -1 
  493    -1         break;
  494    -1       case '>':
  495    -1         if (!partials)
  496    -1           continue;
  497    -1 
  498    -1         value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
  499    -1 
  500    -1         if (value != null)
  501    -1           buffer += this.renderTokens(this.parse(value), context, partials, value);
  502    -1 
  503    -1         break;
  504    -1       case '&':
  505    -1         value = context.lookup(token[1]);
  506    -1 
  507    -1         if (value != null)
  508    -1           buffer += value;
  509    -1 
  510    -1         break;
  511    -1       case 'name':
  512    -1         value = context.lookup(token[1]);
  513    -1 
  514    -1         if (value != null)
  515    -1           buffer += mustache.escape(value);
  516    -1 
  517    -1         break;
  518    -1       case 'text':
  519    -1         buffer += token[1];
  520    -1         break;
  521    -1       }
  522    -1     }
  523    -1 
  524    -1     return buffer;
  525    -1   };
  526    -1 
  527    -1   mustache.name = "mustache.js";
  528    -1   mustache.version = "0.8.1";
  529    -1   mustache.tags = [ "{{", "}}" ];
  530    -1 
  531    -1   // All high-level mustache.* functions use this writer.
  532    -1   var defaultWriter = new Writer();
  533    -1 
  534    -1   /**
  535    -1    * Clears all cached templates in the default writer.
  536    -1    */
  537    -1   mustache.clearCache = function () {
  538    -1     return defaultWriter.clearCache();
  539    -1   };
  540    -1 
  541    -1   /**
  542    -1    * Parses and caches the given template in the default writer and returns the
  543    -1    * array of tokens it contains. Doing this ahead of time avoids the need to
  544    -1    * parse templates on the fly as they are rendered.
  545    -1    */
  546    -1   mustache.parse = function (template, tags) {
  547    -1     return defaultWriter.parse(template, tags);
  548    -1   };
  549    -1 
  550    -1   /**
  551    -1    * Renders the `template` with the given `view` and `partials` using the
  552    -1    * default writer.
  553    -1    */
  554    -1   mustache.render = function (template, view, partials) {
  555    -1     return defaultWriter.render(template, view, partials);
  556    -1   };
  557    -1 
  558    -1   // This is here for backwards compatibility with 0.4.x.
  559    -1   mustache.to_html = function (template, view, partials, send) {
  560    -1     var result = mustache.render(template, view, partials);
  561    -1 
  562    -1     if (isFunction(send)) {
  563    -1       send(result);
  564    -1     } else {
  565    -1       return result;
  566    -1     }
  567    -1   };
  568    -1 
  569    -1   // Export the escaping function so that the user may override it.
  570    -1   // See https://github.com/janl/mustache.js/issues/244
  571    -1   mustache.escape = escapeHtml;
  572    -1 
  573    -1   // Export these mainly for testing, but also for advanced usage.
  574    -1   mustache.Scanner = Scanner;
  575    -1   mustache.Context = Context;
  576    -1   mustache.Writer = Writer;
  577    -1 
  578    -1 }));

diff --git a/plutopluto/static/plutopluto.js b/plutopluto/static/plutopluto.js

@@ -1,119 +1,124 @@
    1    -1 "use strict";
    2    -1 
    3    -1 $(document).ready(function() {
    4    -1 	$.ajax('/config', {success: function(config) {
    5    -1 		var entries = [];
    6    -1 		var page = 0;
    7    -1 
    8    -1 		// load templates
    9    -1 		var template = $('#stream').html();
   10    -1 		Mustache.parse(template);
   11    -1 		$('#stream').html('');
   12    -1 
   13    -1 		var formatDate = function(date, format) {
   14    -1 			var o = {
   15    -1 				'Y': date.getFullYear(),
   16    -1 				'M': date.getMonth() + 1,
   17    -1 				'd': date.getDate(),
   18    -1 				'h': date.getHours(),
   19    -1 				'm': date.getMinutes(),
   20    -1 				's': date.getSeconds
   -1     1 var template = document.querySelector('template');
   -1     2 var stream = document.querySelector('#stream');
   -1     3 var loading = document.querySelector('.loading');
   -1     4 
   -1     5 var formatDate = function(text) {
   -1     6 	var date = new Date(parseInt(text, 10) * 1000);
   -1     7 	var format = 'YYYY/MM/dd hh:mm';
   -1     8 
   -1     9 	var o = {
   -1    10 		'Y': date.getFullYear(),
   -1    11 		'M': date.getMonth() + 1,
   -1    12 		'd': date.getDate(),
   -1    13 		'h': date.getHours(),
   -1    14 		'm': date.getMinutes(),
   -1    15 		's': date.getSeconds,
   -1    16 	};
   -1    17 
   -1    18 	for (var key in o) {
   -1    19 		if (new RegExp('(' + key + '+)').test(format)) {
   -1    20 			var value = o[key].toString();
   -1    21 			if (RegExp.$1.length > 1) {
   -1    22 				value = ('0000' + value).substr(-RegExp.$1.length);
   21    23 			}
   -1    24 			format = format.replace(RegExp.$1, value);
   -1    25 		}
   -1    26 	}
   -1    27 
   -1    28 	return format;
   -1    29 };
   -1    30 
   -1    31 var bottomDistance = function() {
   -1    32 	var doc = document.body.scrollHeight;
   -1    33 	var screen = window.innerHeight;
   -1    34 	var position = document.body.scrollTop || window.scrollY;
   -1    35 	return doc - position - screen;
   -1    36 };
   -1    37 
   -1    38 var escapeHTML = function(text) {
   -1    39 	var p = document.createElement('p');
   -1    40 	p.textContent = text;
   -1    41 	return p.innerHTML;
   -1    42 };
   -1    43 
   -1    44 var appendEntries = function(entries) {
   -1    45 	entries.forEach(entry => {
   -1    46 		var li = document.createElement('li');
   -1    47 		li.className = 'post';
   -1    48 		li.innerHTML = template.innerHTML
   -1    49 			.replace('{{source}}', escapeHTML(entry.source))
   -1    50 			.replace('{{link}}', escapeHTML(entry.link))
   -1    51 			.replace('{{title}}', escapeHTML(entry.title))
   -1    52 			.replace('{{dt}}', escapeHTML(formatDate(entry.dt)))
   -1    53 			.replace('{{{content}}}', entry.content);
   -1    54 		stream.append(li);
   -1    55 	});
   -1    56 };
   -1    57 
   -1    58 var fetchJSON = function(url) {
   -1    59 	return fetch(url).then(r => {
   -1    60 		if (r.ok) {
   -1    61 			return r.json();
   -1    62 		} else {
   -1    63 			throw r;
   -1    64 		}
   -1    65 	});
   -1    66 };
   22    67 
   23    -1 			for (var key in o) {
   24    -1 				if (new RegExp('(' + key + '+)').test(format)) {
   25    -1 					var value = o[key].toString();
   26    -1 					if (RegExp.$1.length > 1) {
   27    -1 						value = ("0000" + value).substr(-RegExp.$1.length);
   28    -1 					}
   29    -1 					format = format.replace(RegExp.$1, value)
   30    -1 				}
   31    -1 			}
   -1    68 fetchJSON('/config').then(config => {
   -1    69 	var entries = [];
   -1    70 	var page = 0;
   32    71 
   33    -1 			return format;
   34    -1 		};
   -1    72 	var loadNextPageLock = false;
   -1    73 	var loadNextPage = function() {
   -1    74 		if (loadNextPageLock) {
   -1    75 			return;
   -1    76 		}
   35    77 
   36    -1 		var formatDateFilter = function() {
   37    -1 			return function(text, render) {
   38    -1 				var dt = new Date(parseInt(render(text), 10) * 1000);
   39    -1 				return formatDate(dt, 'YYYY/MM/dd hh:mm');
   40    -1 			}
   41    -1 		};
   42    -1 
   43    -1 		var bottomDistance = function() {
   44    -1 			var doc = document.body.scrollHeight;
   45    -1 			var screen = window.innerHeight;
   46    -1 			var position = document.body.scrollTop || window.scrollY;
   47    -1 			return doc - position - screen;
   48    -1 		};
   49    -1 
   50    -1 		var appendEntries = function(entries) {
   51    -1 			entries.forEach(function(entry) {
   52    -1 				entry.formatDate = formatDateFilter;
   53    -1 				var rendered = Mustache.render(template, entry);
   54    -1 				$('#stream').append($(rendered));
   55    -1 			});
   56    -1 		};
   57    -1 
   58    -1 		var loadNextPageLock = false;
   59    -1 		var loadNextPage = function() {
   60    -1 			if (!loadNextPageLock) {
   61    -1 				loadNextPageLock = true;
   62    -1 
   63    -1 				config.urls.forEach(function(url) {
   64    -1 					if (url.indexOf('{page}') >= 0) {
   65    -1 						url = url.replace('{page}', page);
   66    -1 					} else if (page !== 0) {
   67    -1 						return;
   68    -1 					}
   69    -1 
   70    -1 					$.ajax('/parse', {
   71    -1 						data: {url: url},
   72    -1 						success: function(data) {
   73    -1 							entries = entries.concat(data.entries);
   74    -1 
   75    -1 							// now that we have entries, we can show some
   76    -1 							if ($('#stream').children().length === 0) {
   77    -1 								loadMore();
   78    -1 							}
   79    -1 
   80    -1 							// ideally we would wait until all requests have finished
   81    -1 							// but this is only a simple optimisation anyway
   82    -1 							loadNextPageLock = false;
   83    -1 						}
   84    -1 					});
   85    -1 				});
   86    -1 				page++;
   -1    78 		loadNextPageLock = true;
   -1    79 
   -1    80 		var promises = config.urls.map(url => {
   -1    81 			if (url.includes('{page}')) {
   -1    82 				url = url.replace('{page}', page);
   -1    83 			} else if (page !== 0) {
   -1    84 				return;
   87    85 			}
   88    -1 		};
   89    86 
   90    -1 		var loadMore = function() {
   91    -1 			entries.sort(function(a, b) {
   92    -1 				return b.dt - a.dt;
   -1    87 			return fetchJSON('/parse?' + new URLSearchParams({url: url})).then(data => {
   -1    88 				entries = entries.concat(data.entries);
   93    89 			});
   94    -1 			appendEntries(entries.splice(0, 10));
   95    -1 			if (entries.length < 30) {
   96    -1 				loadNextPage();
   97    -1 			}
   98    -1 		};
   -1    90 		});
   99    91 
  100    -1 		$(document).on('scroll', function() {
  101    -1 			if (bottomDistance() < 4000) {
   -1    92 		Promise.all(promises).finally(() => {
   -1    93 			// now that we have entries, we can show some
   -1    94 			if (stream.children.length === 0) {
  102    95 				loadMore();
  103    96 			}
  104    -1 		}, {passive: true});
  105    -1 		$('.loading').click(loadMore);
  106    -1 
  107    -1 		// load initial content
  108    -1 		loadNextPage();
  109    -1 	}});
  110    -1 
  111    -1 	// youtube video embedding
  112    -1 	$(document).on('click', function(event) {
  113    -1 		if (event.target.alt.match(/^https:\/\/www\.youtube/)) {
  114    -1 			var iframe = $('<iframe class="youtube">');
  115    -1 			iframe.target.src = event.target.alt;
  116    -1 			$(event.target).replace(iframe);
   -1    97 
   -1    98 			loadNextPageLock = false;
   -1    99 		});
   -1   100 
   -1   101 		page++;
   -1   102 	};
   -1   103 
   -1   104 	var loadMore = function() {
   -1   105 		entries.sort((a, b) => {
   -1   106 			return b.dt - a.dt;
   -1   107 		});
   -1   108 		appendEntries(entries.splice(0, 10));
   -1   109 		if (entries.length < 30) {
   -1   110 			loadNextPage();
  117   111 		}
  118    -1 	});
   -1   112 	};
   -1   113 
   -1   114 	document.addEventListener('scroll', () => {
   -1   115 		if (bottomDistance() < 4000) {
   -1   116 			loadMore();
   -1   117 		}
   -1   118 	}, {passive: true});
   -1   119 
   -1   120 	loading.addEventListener('click', loadMore);
   -1   121 
   -1   122 	// load initial content
   -1   123 	loadNextPage();
  119   124 });