Whoops, committed a local baseURL change
[blerg.git] / www / js / blerg.js
1 /* Blerg is (C) 2011 The Dominion of Awesome, and is distributed under a
2  * BSD-style license.  Please see the COPYING file for details.
3  */
4
5 // Config
6 var baseURL = '';
7 var recordTemplate = new Template(
8     '<div class="record">#{data}<div class="info">Posted #{date}. <a href="' + baseURL + '/\##{author}/#{record}" onclick="return qlink()">[permalink]</a> <a href="#" onclick="postPopup(\'@#{author}/#{record}: \'); return false">[reply]</a></div></div>'
9 );
10 var tagRecordTemplate = new Template(
11     '<div class="record">#{data}<div class="info">Posted by <a class="author ref" href="/\##{author}" onclick="return qlink()">@#{author}</a> on #{date}. <a href="' + baseURL + '/\##{author}/#{record}" onclick="return qlink()">[permalink]</a> <a href="#" onclick="postPopup(\'@#{author}/#{record}: \'); return false">[reply]</a></div></div>'
12 );
13 var latestRecordsTemplate = new Template(
14     '<div class="record"><a class="author ref" href="' + baseURL + '/\##{author}" onclick="return qlink()">@#{author}</a> #{data}</div>'
15 );
16
17 // Page elements
18 var items;
19
20 // Other globals
21 var currentPager;
22 var loginStatus;
23
24 // Object to keep track of login status
25 function LoginStatus() {
26     var cookies = {};
27     document.cookie.split(/;\s+/).each(function(v) {
28         kv = v.split('=');
29         cookies[kv[0]] = kv[1];
30     });
31     if (cookies.auth && cookies.username) {
32         this.loggedIn = true;
33         this.username = cookies.username;
34     } else {
35         this.loggedIn = false;
36         this.username = null;
37     }
38     this.update();
39 }
40
41 LoginStatus.prototype.login = function(username, password) {
42     new Ajax.Request(baseURL + '/login', {
43         parameters: {
44             username: username,
45             password: password
46         },
47         onSuccess: function(r) {
48             var j = r.responseText.evalJSON();
49             if (j && j.status == 'success') {
50                 this.loggedIn = true;
51                 this.username = username;
52                 document.cookie = "username=" + username;
53                 $('login.password').value = '';
54                 this.update();
55             } else {
56                 alert("Could not log in");
57                 $('login.username').focus();
58             }
59         }.bind(this),
60         onFailure: function(r) {
61             alert("Could not log in");
62             $('login.username').focus();
63         }
64     });
65 }
66
67 LoginStatus.prototype.logout = function() {
68     new Ajax.Request(baseURL + '/logout', {
69         parameters: {
70             username: this.username
71         },
72         onComplete: function(r) {
73             this.loggedIn = false;
74             document.cookie = "auth=; expires=1-Jan-1970 00:00:00 GMT";
75             this.update();
76         }.bind(this)
77     });
78     document.cookie = "username=; expires=1-Jan-1970 00:00:00 GMT";
79 }
80
81 LoginStatus.prototype.update = function() {
82     if (this.loggedIn) {
83         $('userlink').href = baseURL + '/#' + this.username;
84         $('userlink').update('@' + this.username);
85         $('reflink').href = baseURL + '/#/ref/' + this.username;
86         $('login').hide();
87         $('logout').show();
88     } else {
89         $('post').hide();
90         $('login').show();
91         $('logout').hide();
92     }
93 }
94
95 LoginStatus.prototype.post = function(msg) {
96     if (!this.loggedIn) {
97         alert("You are not logged in!");
98         return;
99     }
100
101     new Ajax.Request(baseURL + '/put', {
102         parameters: {
103             username: this.username,
104             data: msg
105         },
106         onSuccess: function(r) {
107             var j = r.responseText.evalJSON();
108             if (j && j.status == 'success') {
109                 $('post.content').value = '';
110                 if (location.hash != '#' + this.username) {
111                     qlink(this.username);
112                 } else {
113                     currentPager.itemCount++;
114                     currentPager.reload();
115                 }
116             } else {
117                 alert('Post failed!');
118             }
119         }.bind(this),
120         onFailure: function(r) {
121             alert('Post failed!');
122         }
123     });
124 }
125
126
127 // Base object for paged data
128 function Pager() {
129     this.itemsPerPage = 10;
130     this.itemCache = new Hash();
131     this.pageStart = null;
132 }
133
134 Pager.prototype.updateState = function(m) {
135     return false;
136 }
137
138 Pager.prototype.show = function() {
139     items.show();
140 }
141
142 Pager.prototype.hide = function() {
143     items.hide();
144     items.update();
145     $('newer_link').hide();
146     $('older_link').hide();
147 }
148
149 Pager.prototype.olderPage = function() {
150     if (this.pageStart >= this.itemsPerPage) {
151         qlink(this.baseFrag + '/p' + (this.pageStart - this.itemsPerPage));
152     }
153 }
154
155 Pager.prototype.newerPage = function() {
156     if (this.pageStart + this.itemsPerPage < this.itemCount) {
157         qlink(this.baseFrag + '/p' + (this.pageStart + this.itemsPerPage));
158     }
159 }
160
161 Pager.prototype.addItems = function(items) {
162     items.each(function(v) {
163         if (!this.itemCache[v.id])
164             this.itemCache[v.id] = v;
165     }.bind(this));
166 }
167
168 Pager.prototype.displayItems = function() {
169     if (this.pageStart == undefined)
170         this.pageStart == this.itemCount - 1;
171     items.update();
172
173     if (this.pageStart != undefined && this.itemCache[this.pageStart]) {
174         var end = (this.pageStart >= this.itemsPerPage ? this.pageStart - this.itemsPerPage + 1 : 0);
175         for (var i = this.pageStart; i >= end; i--) {
176             items.insert(this.itemCache[i].html);
177         }
178     } else {
179         items.insert("There doesn't seem to be anything here!");
180     }
181
182     if (this.pageStart < this.itemCount - 1) {
183         $('newer_link').href = baseURL + '/#' + this.baseFrag + '/p' + (this.pageStart + this.itemsPerPage);
184         $('newer_link').show();
185     } else {
186         $('newer_link').hide();
187     }
188
189     if (this.pageStart >= 10) {
190         $('older_link').href = baseURL + '/#' + this.baseFrag + '/p' + (this.pageStart - this.itemsPerPage);
191         $('older_link').show();
192     } else {
193         $('older_link').hide();
194     }
195
196     document.body.scrollTo();
197 }
198
199 Pager.prototype.reload = function() {
200     this.pageStart = null;
201     this.loadItems(null, null, Pager.prototype.showPageAt.bind(this, this.itemCount - 1));
202 }
203
204 Pager.prototype.showPageAt = function(r) {
205     var end = (r - 9 > 0 ? r - 9 : 0);
206     if (this.itemCache[r] && this.itemCache[end]) {
207         this.pageStart = r;
208         this.displayItems();
209     } else {
210         this.loadItems((r >= 49 ? r - 49 : 0), r, Pager.prototype.showPageAt.bind(this, r));
211     }
212 }
213
214 Pager.prototype.showRecord = function(r) {
215     if (this.itemCache[r]) {
216         $('older_link').hide();
217         $('newer_link').hide();
218         items.update(this.itemCache[r].html);
219     } else {
220         this.loadItems(r, r, Pager.prototype.showRecord.bind(this, r));
221     }
222 }
223
224 Pager.prototype.loadItems = function(from, to, continuation) { }
225
226
227 // Object to render user pages
228 function User(m) {
229     Pager.call(this);
230     this.username = m[1];
231     this.baseFrag = m[1];
232     this.permalink = (m[2] != 'p');
233     this.pageStart = parseInt(m[3]);
234 }
235 User.prototype = new Pager();
236 User.prototype.constructor = User;
237
238 User.prototype.updateState = function(m) {
239     if (m[1] != this.username)
240         return false;
241     
242     this.permalink = (m[2] != 'p');
243     this.pageStart = parseInt(m[3]);
244     this.show();
245
246     return true;
247 }
248
249 User.prototype.show = function() {
250     Pager.prototype.show.call(this);
251
252     $$('[name=section]').each(function(v) { v.update(' @' + this.username) }.bind(this));
253     $('rss').show();
254     $('rsslink').href = '/rss/' + this.username;
255     $$('[name=user.reflink]').each(function(e) {
256         e.href = baseURL + '/#/ref/' + this.username;
257     }.bind(this));
258     $('usercontrols').show();
259
260     if (this.permalink && this.pageStart >= 0) {
261         this.showRecord(this.pageStart);
262     } else if (this.pageStart >= 0) {
263         this.showPageAt(this.pageStart);
264     } else {
265         this.reload();
266     }
267 }
268
269 User.prototype.hide = function() {
270     Pager.prototype.hide.call(this);
271     $('signup').hide();
272     $('rss').hide();
273     $('usercontrols').hide();
274 }
275
276 User.prototype.reload = function() {
277     this.pageStart = null;
278
279     $$('[name=user.subscribelink]').each(Element.hide);
280     $$('[name=user.unsubscribelink]').each(Element.hide);
281
282     if (loginStatus.loggedIn) {
283         new Ajax.Request(baseURL + '/feedinfo/' + this.username, {
284             method: 'post',
285             parameters: {
286                 username: loginStatus.username
287             },
288             onSuccess: function(r) {
289                 var json = r.responseText.evalJSON();
290                 if (json.subscribed) {
291                     $$('[name=user.subscribelink]').each(Element.hide);
292                     $$('[name=user.unsubscribelink]').each(Element.show);
293                 } else {
294                     $$('[name=user.subscribelink]').each(Element.show);
295                     $$('[name=user.unsubscribelink]').each(Element.hide);
296                 }
297             }
298         });
299     }
300
301     new Ajax.Request(baseURL + '/info/' + this.username, {
302         method: 'get',
303         onSuccess: function(r) {
304             var response = r.responseText.evalJSON();
305             if (response) {
306                 this.itemCount = parseInt(response.record_count);
307                 this.showPageAt(this.itemCount - 1);
308             }
309         }.bind(this)
310     });
311 }
312
313 User.prototype.loadItems = function(from, to, continuation) {
314     if (to < 0)
315         return;
316
317     var url;
318     if (from != undefined && to != undefined) {
319         url = baseURL + '/get/' + this.username + '/' + from + '-' + to;
320         this.pageStart = to;
321     } else {
322         url = baseURL + '/get/' + this.username;
323     }
324
325     new Ajax.Request(url, {
326         method: 'get',
327         onSuccess: function(r) {
328             var records = r.responseText.evalJSON();
329             if (records && records.length > 0) {
330                 records.each(function(v) {
331                     v.id = v.record;
332                     v.author = this.username;
333                     mangleRecord(v, recordTemplate);
334                 }.bind(this));
335                 this.addItems(records);
336                 if (!this.pageStart)
337                     this.pageStart = records[0].recInt;
338             }
339             continuation();
340         }.bind(this),
341         onFailure: function(r) {
342             this.displayItems();
343         }.bind(this),
344         on404: function(r) {
345             displayError('User not found');
346         }
347     });
348 }
349
350 function mangleRecord(record, template) {
351     record.recInt = parseInt(record.record);
352
353     var lines = record.data.split(/\r?\n/);
354     if (lines[lines.length - 1] == '')
355         lines.pop();
356
357     var out = ['<p>'];
358     var endpush = null;
359     var listMode = false;
360     lines.each(function(l) {
361         if (l == '') {
362             if (out[out.length - 1] == '<br>') {
363                 out[out.length - 1] = '<p>';
364             }
365             if (out[out.length - 1] == '</li>') {
366                 out.push('</ul>');
367                 out.push('<p>');
368                 listMode = false;
369             }
370             return;
371         }
372
373         // Put quoted material into a special paragraph
374         if (l[0] == '>') {
375             var pi = out.lastIndexOf('<p>');
376             if (pi != -1) {
377                 out[pi] = '<p class="quote">';
378                 l = l.replace(/^>\s*/, '');
379             }
380         }
381
382         // Sanitize HTML input
383         l = l.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
384
385         // Turn HTTP URLs into links
386         l = l.replace(/(\s|^)(https?:\/\/[a-zA-Z0-9.-]*[a-zA-Z0-9](\/([^\s"]*[^.!,;?()\s])?)?)/g, '$1<a href="$2">$2</a>');
387
388         // Turn markdown links into links
389         l = l.replace(/(\s|^)\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9.-]*[a-zA-Z0-9](\/[^)"]*?)?)\)/g, '$1<a href="$3">$2</a>');
390
391         // Turn *foo* into italics and **foo** into bold
392         l = l.replace(/([^\w\\]|^)\*\*(\w[^*]*)\*\*(\W|$)/g, '$1<b>$2</b>$3');
393         l = l.replace(/([^\w\\]|^)\*(\w[^*]*)\*(\W|$)/g, '$1<i>$2</i>$3');
394
395         // Turn refs and tags into links
396         l = l.replace(/(\s|^)#([A-Za-z0-9_-]+)/g, '$1<a href="#/tag/$2" class="ref" onclick="return qlink()">#$2</a>');
397         l = l.replace(/(\s|^)@([A-Za-z0-9_-]+)(\/\d+)?/g, '$1<a href="#$2$3" class="ref" onclick="return qlink()">@$2</a>');
398
399         // Create lists when lines begin with *
400         if (l[0] == '*') {
401             if (!listMode) {
402                 var pi = out.lastIndexOf('<p>');
403                 out[pi] = '<ul>';
404                 listMode = true;
405             }
406             l = l.replace(/^\*\s*/, '');
407             out.push('<li>');
408             endpush = '</li>';
409         }
410
411         // Create headers when lines begin with = or #
412         if (l[0] == '=' || l[0] == '#') {
413             var m = l.match(/^([=#]+)/);
414             var depth = m[1].length;
415             if (depth <= 5) {
416                 l = l.replace(/^[=#]+\s*/, '').replace(/\s*[=#]+$/, '');
417                 out.push('<h' + depth + '>');
418                 endpush = '</h' + depth + '>';
419             }
420         }
421
422         // Remove backslashes from escaped metachars
423         l = l.replace(/\\([*\[\]@#])/g, '$1');
424
425         out.push(l);
426         if (endpush) {
427             out.push(endpush);
428             endpush = null;
429         } else {
430             out.push('<br>');
431         }
432     });
433     while (out[out.length - 1] == '<br>' || out[out.length - 1] == '<p>')
434         out.pop();
435     if (listMode)
436         out.push('</ul>');
437
438     record.data = out.join('');
439     record.date = (new Date(record.timestamp * 1000)).toString();
440     record.html = template.evaluate(record);
441 }
442
443 function displayError(msg) {
444     items.innerText = msg;
445 }
446
447
448 // Object for browsing tags
449 function Tag(m) {
450     Pager.call(this);
451     this.type = m[1];
452     this.tag = m[2];
453     this.pageStart = parseInt(m[3]);
454
455     var url = baseURL + "/tag/";
456     switch(this.type) {
457     case 'tag':
458         //url += '%23';
459         url += 'H';  // apache is eating the hash, even encoded.  Probably a security feature.
460         this.baseFrag = '/tag/' + this.tag;
461         break;
462     case 'ref':
463         url += '%40';
464         this.baseFrag = '/ref/' + this.tag;
465         break;
466     default:
467         alert('Invalid tag type: ' + this.type);
468         return;
469     }
470     url += this.tag;
471
472     new Ajax.Request(url, {
473         method: 'get',
474         onSuccess: function(r) {
475             var j = r.responseText.evalJSON();
476             if (j) {
477                 var maxid = j.length - 1;
478                 j.each(function(v, i) {
479                     v.id = maxid - i;
480                     mangleRecord(v, tagRecordTemplate)
481                 });
482                 this.addItems(j);
483                 if (!this.pageStart)
484                     this.pageStart = j.length - 1;
485                 this.itemCount = j.length;
486             }
487             this.displayItems();
488         }.bind(this),
489         onFailure: function(r) {
490             this.displayItems();
491         }.bind(this)
492     });
493
494 }
495 Tag.prototype = new Pager();
496 Tag.prototype.constructor = Tag;
497
498 Tag.prototype.updateState = function(m) {
499     if (this.type != m[1] || this.tag != m[2])
500         return false;
501
502     this.pageStart = parseInt(m[3]) || this.itemCount - 1;
503     this.displayItems();
504
505     return true;
506 }
507
508 Tag.prototype.show = function() {
509     Pager.prototype.show.call(this);
510
511     var ctype = {ref: '@', tag: '#'}[this.type];
512
513     $$('[name=section]').each(function(v) {
514         v.update(' about ' + ctype + this.tag);
515     }.bind(this));
516 }
517
518
519 // Pager for browsing subscription feeds
520 function Feed(m) {
521     Pager.call(this);
522     this.username = loginStatus.username;
523     this.baseFrag = '/feed';
524     this.pageStart = parseInt(m[1]);
525
526     new Ajax.Request(baseURL + '/feed', {
527         method: 'post',
528         parameters: {
529             username: loginStatus.username
530         },
531         onSuccess: function(r) {
532             var response = r.responseText.evalJSON();
533             if (response) {
534                 var maxid = response.length - 1;
535                 response.each(function(v, i) {
536                     v.id = maxid - i;
537                     mangleRecord(v, tagRecordTemplate)
538                 });
539                 this.addItems(response);
540                 if (!this.pageStart)
541                     this.pageStart = response.length - 1;
542                 this.itemCount = response.length;
543             }
544             this.displayItems();
545         }.bind(this),
546         onFailure: function(r) {
547             this.displayItems();
548         }.bind(this)
549     });
550 }
551 Feed.prototype = new Pager();
552 Feed.prototype.constructor = Feed;
553
554 Feed.prototype.updateState = function(m) {
555     this.pageStart = parseInt(m[1]) || this.itemCount - 1;
556     this.displayItems();
557
558     return true;
559 }
560
561 Feed.prototype.show = function() {
562     Pager.prototype.show.call(this);
563     $$('[name=section]').each(function(v) {
564         v.update(' ' + loginStatus.username + "'s spycam");
565     }.bind(this));
566 }
567
568
569 function postPopup(initial) {
570     if (loginStatus.loggedIn || initial) {
571         var post = $('post');
572         if (post.visible()) {
573             post.hide();
574         } else {
575             post.show();
576             if (initial) {
577                 $('post.content').value = initial;
578             } else if (!$('post.content').value && currentPager.username && currentPager.username != loginStatus.username) {
579                 $('post.content').value = '@' + currentPager.username + ': ';
580             }
581             $('post.content').focus();
582         }
583     }
584 }
585
586 function signup() {
587     var username = $('signup.username').value;
588     var password = $('signup.password').value;
589
590     new Ajax.Request(baseURL + '/create', {
591         parameters: {
592             username: username,
593             password: password
594         },
595         onSuccess: function(r) {
596             $('signup').hide();
597             qlink(username);
598
599             loginStatus.login(username, password);
600         },
601         onFailure: function(r) {
602             alert("Failed to create user");
603         }
604     });
605 }
606
607 function signup_cancel() {
608     $('signup').hide();
609     urlSwitch();
610 }
611
612 function subscribe() {
613     new Ajax.Request(baseURL + '/subscribe/' + currentPager.username, {
614         method: 'post',
615         parameters: {
616             username: loginStatus.username
617         },
618         onSuccess: function(r) {
619             var response = r.responseText.evalJSON();
620             if (response.status == 'success') {
621                 alert("You call " + currentPager.username + " and begin breathing heavily into the handset.");
622                 $$('[name=user.subscribelink]').each(Element.hide);
623                 $$('[name=user.unsubscribelink]').each(Element.show);
624             } else {
625                 alert('Failed to subscribe. This is probably for the best');
626             }
627         },
628         onFailure: function(r) {
629             alert('Failed to subscribe. This is probably for the best');
630         }
631     });
632 }
633
634 function unsubscribe() {
635     new Ajax.Request(baseURL + '/unsubscribe/' + currentPager.username, {
636         method: 'post',
637         parameters: {
638             username: loginStatus.username
639         },
640         onSuccess: function(r) {
641             var response = r.responseText.evalJSON();
642             if (response.status == 'success') {
643                 alert("You come to your senses.");
644                 $$('[name=user.subscribelink]').each(Element.show);
645                 $$('[name=user.unsubscribelink]').each(Element.hide);
646             } else {
647                 alert('You are unable to tear yourself away (because something failed on the server)');
648             }
649         },
650         onFailure: function(r) {
651             alert('You are unable to tear yourself away (because something failed on the server)');
652         }
653     });
654 }
655
656 var resizePostContentTimeout = null;
657 function resizePostContent() {
658     if (resizePostContentTimeout)
659         clearTimeout(resizePostContentTimeout);
660     resizePostContentTimeout = setTimeout(function() {
661         var c = $('post.content');
662         var lines = Math.floor(c.value.length / (100 * (c.clientWidth / 1000))) + 1;
663         var m = c.value.match(/\r?\n/g);
664         if (m)
665             lines += m.length;
666         if (lines <= 3) {
667             c.style.height = "";
668         } else {
669             c.style.height = (lines * 17) + "pt";
670         }
671         resizePostContentTimeout = null;
672     }, 150);
673 }
674
675 var tickerTimer = null;
676 var tickerHead, tickerTail;
677
678 function tickerFader(a, b, p) {
679     var p2 = 1 - p;
680
681     a.style.opacity = p;
682     a.style.lineHeight = (100 * p) + '%';
683
684     b.style.opacity = p2;
685     b.style.lineHeight = (100 * p2) + '%';
686     if (p == 1.0)
687         b.hide();
688 }
689
690 function ticker() {
691     tickerHead.show();
692     Bytex64.FX.run(tickerFader.curry(tickerHead, tickerTail), 0.5);
693     tickerHead = tickerHead.nextSibling;
694     tickerTail = tickerTail.nextSibling;
695     if (tickerHead == null) {
696         stopTicker();
697         loadLatest.delay(10);
698     }
699 }
700
701 function startTicker() {
702     stopTicker();
703     for (var elem = $('latest-posts').firstChild; elem != null; elem = elem.nextSibling) {
704         elem.hide();
705     }
706
707     // Show the first five
708     tickerHead = $('latest-posts').firstChild;
709     for (var i = 0; i < 10 && tickerHead; i++) {
710         tickerHead.show();
711         tickerHead = tickerHead.nextSibling;
712     }
713     tickerTail = $('latest-posts').firstChild;
714     tickerTimer = setInterval(ticker, 5000);
715 }
716
717 function stopTicker() {
718     if (tickerTimer)
719         clearInterval(tickerTimer);
720     tickerTimer = null;
721 }
722
723 function loadLatest() {
724     new Ajax.Request(baseURL + '/latest.json', {
725         method: 'GET',
726         onSuccess: function(r) {
727             var j = r.responseText.evalJSON();
728
729             $('latest-tags').update();
730             j.tags.each(function(v) {
731                 var a = new Element('a', {href: baseURL + '/#/tag/' + v});
732                 a.insert('#' + v);
733                 a.onclick = "return qlink()";
734                 a.className = 'ref';
735                 $('latest-tags').insert(a);
736                 $('latest-tags').appendChild(document.createTextNode(' '));
737             });
738
739             $('latest-posts').update();
740             j.records.each(function(v) {
741                 v.data = v.data.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
742                 v.date = (new Date(v.timestamp * 1000)).toString();
743                 var html = latestRecordsTemplate.evaluate(v);
744                 $('latest-posts').insert(html);
745             });
746             startTicker();
747         }
748     });
749 }
750
751 function qlink(loc) {
752     if (loc) {
753         location.hash = loc;
754     } else if (event && event.target) {
755         location.href = event.target.href;
756     } else {
757         // Bogus qlink
758         return;
759     }
760     lastHash = location.hash;
761     urlSwitch();
762     return false;
763 }
764
765 function Welcome() {
766     loadLatest();
767 }
768
769 Welcome.prototype.show = function() {
770     $$('[name=section]').each(function(v) { v.update('Welcome') });
771     $('welcome').show();
772 }
773
774 Welcome.prototype.hide = function() {
775     stopTicker();
776     $('welcome').hide();
777 }
778
779 function ExternalURLPost(m) {
780     this.title = decodeURIComponent(m[1]).replace(']','').replace('[','');
781     this.url = decodeURIComponent(m[2]);
782 }
783
784 ExternalURLPost.prototype.show = function() {
785     $('post.content').value = '[' + this.title + '](' + this.url + ')';
786     $('post').show();
787 }
788
789 var urlmap = [
790     ['search', /^\?post\/([^/]+)\/(.+)/, ExternalURLPost],
791     ['hash',   /^#\/(ref|tag)\/([A-Za-z0-9_-]+)(?:\/p(\d+))?$/, Tag],
792     ['hash',   /^#\/feed(?:\/p(\d+))?$/, Feed],
793     ['hash',   /^#([A-Za-z0-9_-]+)(?:\/(p)?(\d+))?$/, User]
794 ];
795
796 function urlSwitch() {
797     var m;
798     var pageconstructor;
799
800     for (var i = 0; i < urlmap.length; i++) {
801         if (m = location[urlmap[i][0]].match(urlmap[i][1])) {
802             pageconstructor = urlmap[i][2];
803             break;
804         }
805     }
806     if (i == urlmap.length)
807         pageconstructor = Welcome;
808
809     if (currentPager && currentPager instanceof pageconstructor) {
810         // updateState returns true if the state has been successfully updated.
811         // Otherwise, we continue and create a new instance.
812         if (currentPager.updateState(m))
813             return;
814     }
815
816     if (currentPager && currentPager.hide)
817         currentPager.hide();
818
819     currentPager = new pageconstructor(m);
820     if (currentPager.show)
821         currentPager.show();
822 }
823
824 var lastHash;
825 function hashCheck() {
826     if (location.hash != lastHash) {
827         lastHash = location.hash;
828         urlSwitch();
829     }
830 }
831
832 function init() {
833     items = $('items');
834     loginStatus = new LoginStatus();
835
836     lastHash = location.hash;
837     urlSwitch();
838
839     setInterval(hashCheck, 250);
840
841     document.body.observe('keyup', function(event) {
842         if (event.shiftKey && event.keyCode == 32) {
843             postPopup();
844             event.stop();
845         }
846     });
847     $('post.content').addEventListener('keyup', function(event) {
848         event.stopPropagation();
849     }, true);
850 }