someone_

4chanX 2.39.6_namespace

Apr 20th, 2014
204
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name           4chan x
  3. // @version        2.39.6
  4. // @namespace      aeosynth
  5. // @description    Adds various features.
  6. // @copyright      2009-2011 James Campos <james.r.campos@gmail.com>
  7. // @copyright      2012-2013 Nicolas Stepien <stepien.nicolas@gmail.com>
  8. // @license        MIT; http://en.wikipedia.org/wiki/Mit_license
  9. // @include        http://boards.4chan.org/*
  10. // @include        https://boards.4chan.org/*
  11. // @include        http://i.4cdn.org/*
  12. // @include        https://i.4cdn.org/*
  13. // @include        http://sys.4chan.org/*
  14. // @include        https://sys.4chan.org/*
  15. // @grant          GM_getValue
  16. // @grant          GM_setValue
  17. // @grant          GM_deleteValue
  18. // @grant          GM_openInTab
  19. // @run-at         document-start
  20. // @updateURL      https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js
  21. // @downloadURL    https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js
  22. // @icon           data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAIALAAAAAAQABAAAAIxlI+pq+D9DAgUoFkPDlbs7lGiI2bSVnKglnJMOL6omczxVZK3dH/41AG6Lh7i6qUoAAA7
  23. // ==/UserScript==
  24.  
  25. /* LICENSE
  26.  *
  27.  * Copyright (c) 2009-2011 James Campos <james.r.campos@gmail.com>
  28.  * Copyright (c) 2012-2013 Nicolas Stepien <stepien.nicolas@gmail.com>
  29.  * http://mayhemydg.github.io/4chan-x/
  30.  * 4chan X 2.39.6
  31.  *
  32.  * Permission is hereby granted, free of charge, to any person
  33.  * obtaining a copy of this software and associated documentation
  34.  * files (the "Software"), to deal in the Software without
  35.  * restriction, including without limitation the rights to use,
  36.  * copy, modify, merge, publish, distribute, sublicense, and/or sell
  37.  * copies of the Software, and to permit persons to whom the
  38.  * Software is furnished to do so, subject to the following
  39.  * conditions:
  40.  *
  41.  * The above copyright notice and this permission notice shall be
  42.  * included in all copies or substantial portions of the Software.
  43.  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  44.  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  45.  * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  46.  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  47.  * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  48.  * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  49.  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  50.  * OTHER DEALINGS IN THE SOFTWARE.
  51.  *
  52.  * HACKING
  53.  *
  54.  * 4chan X is written in CoffeeScript[1], and developed on GitHub[2].
  55.  *
  56.  * [1]: http://coffeescript.org/
  57.  * [2]: https://github.com/MayhemYDG/4chan-x
  58.  *
  59.  * CONTRIBUTORS
  60.  *
  61.  * noface - unique ID fixes
  62.  * desuwa - Firefox filename upload fix
  63.  * seaweed - bottom padding for image hover
  64.  * e000 - cooldown sanity check
  65.  * ahodesuka - scroll back when unexpanding images, file info formatting
  66.  * Shou- - pentadactyl fixes
  67.  * ferongr - new favicons
  68.  * xat- - new favicons
  69.  * Zixaphir - fix qr textarea - captcha-image gap
  70.  * Ongpot - sfw favicon
  71.  * thisisanon - nsfw + 404 favicons
  72.  * Anonymous - empty favicon
  73.  * Seiba - chrome quick reply focusing
  74.  * herpaderpderp - recaptcha fixes
  75.  * WakiMiko - recaptcha tab order http://userscripts.org/scripts/show/82657
  76.  * btmcsweeney - allow users to specify text for sauce links
  77.  *
  78.  * All the people who've taken the time to write bug reports.
  79.  *
  80.  * Thank you.
  81.  */
  82.  
  83. (function() {
  84.   var $, $$, Anonymize, ArchiveLink, AutoGif, Build, CatalogLinks, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, ImageExpand, ImageHover, Keybinds, Main, Menu, Nav, Options, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, StrikethroughQuotes, ThreadHiding, ThreadStats, Time, TitlePost, UI, Unread, Updater, Watcher, d, g, _base;
  85.  
  86.   String.prototype.reverse = function(){return this.split("").reverse().join("")};
  87.  
  88.   Config = {
  89.     main: {
  90.       Enhancing: {
  91.         'Disable 4chan\'s extension': [true, 'Avoid conflicts between 4chan X and 4chan\'s inline extension'],
  92.         'Catalog Links': [true, 'Turn Navigation links into links to each board\'s catalog'],
  93.         '404 Redirect': [true, 'Redirect dead threads and images'],
  94.         'Keybinds': [true, 'Binds actions to keys'],
  95.         'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time'],
  96.         'Relative Post Dates': [false, 'Display dates as "3 minutes ago" f.e., tooltip shows the timestamp'],
  97.         'File Info Formatting': [true, 'Reformats the file information'],
  98.         'Comment Expansion': [true, 'Expand too long comments'],
  99.         'Thread Expansion': [true, 'View all replies'],
  100.         'Index Navigation': [true, 'Navigate to previous / next thread'],
  101.         'Reply Navigation': [false, 'Navigate to top / bottom of thread'],
  102.         'Check for Updates': [true, 'Check for updated versions of 4chan X']
  103.       },
  104.       Filtering: {
  105.         'Anonymize': [false, 'Make everybody anonymous'],
  106.         'Filter': [true, 'Self-moderation placebo'],
  107.         'Recursive Filtering': [true, 'Filter replies of filtered posts, recursively'],
  108.         'Reply Hiding': [true, 'Hide single replies'],
  109.         'Thread Hiding': [true, 'Hide entire threads'],
  110.         'Show Stubs': [true, 'Of hidden threads / replies']
  111.       },
  112.       Imaging: {
  113.         'Image Auto-Gif': [false, 'Animate gif thumbnails'],
  114.         'Image Expansion': [true, 'Expand images'],
  115.         'Image Hover': [false, 'Show full image on mouseover'],
  116.         'Sauce': [true, 'Add sauce to images'],
  117.         'Reveal Spoilers': [false, 'Replace spoiler thumbnails by the original thumbnail'],
  118.         'Expand From Current': [false, 'Expand images from current position to thread end']
  119.       },
  120.       Menu: {
  121.         'Menu': [true, 'Add a drop-down menu in posts'],
  122.         'Report Link': [true, 'Add a report link to the menu'],
  123.         'Delete Link': [true, 'Add post and image deletion links to the menu'],
  124.         'Download Link': [true, 'Add a download with original filename link to the menu (Chrome only)'],
  125.         'Archive Link': [true, 'Add an archive link to the menu']
  126.       },
  127.       Monitoring: {
  128.         'Thread Updater': [true, 'Update threads. Has more options in its own dialog.'],
  129.         'Unread Count': [true, 'Show unread post count in tab title'],
  130.         'Unread Favicon': [true, 'Show a different favicon when there are unread posts'],
  131.         'Post in Title': [true, 'Show the op\'s post in the tab title'],
  132.         'Thread Stats': [true, 'Display reply and image count'],
  133.         'Thread Watcher': [true, 'Bookmark threads'],
  134.         'Auto Watch': [true, 'Automatically watch threads that you start'],
  135.         'Auto Watch Reply': [false, 'Automatically watch threads that you reply to']
  136.       },
  137.       Posting: {
  138.         'Quick Reply': [true, 'Reply without leaving the page'],
  139.         'Cooldown': [true, 'Prevent "flood detected" errors'],
  140.         'Persistent QR': [false, 'The Quick reply won\'t disappear after posting'],
  141.         'Auto Hide QR': [true, 'Automatically hide the quick reply when posting'],
  142.         'Open Reply in New Tab': [false, 'Open replies in a new tab that are made from the main board'],
  143.         'Remember QR size': [false, 'Remember the size of the Quick reply (Firefox only)'],
  144.         'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting'],
  145.         'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting'],
  146.         'Hide Original Post Form': [true, 'Replace the normal post form with a shortcut to open the QR']
  147.       },
  148.       Quoting: {
  149.         'Quote Backlinks': [true, 'Add quote backlinks'],
  150.         'OP Backlinks': [false, 'Add backlinks to the OP'],
  151.         'Quote Highlighting': [true, 'Highlight the previewed post'],
  152.         'Quote Inline': [true, 'Show quoted post inline on quote click'],
  153.         'Quote Preview': [true, 'Show quote content on hover'],
  154.         'Resurrect Quotes': [true, 'Linkify dead quotes to archives'],
  155.         'Indicate OP quote': [true, 'Add \'(OP)\' to OP quotes'],
  156.         'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes'],
  157.         'Forward Hiding': [true, 'Hide original posts of inlined backlinks']
  158.       }
  159.     },
  160.     filter: {
  161.       name: ['# Filter any namefags:', '#/^(?!Anonymous$)/'].join('\n'),
  162.       uniqueid: ['# Filter a specific ID:', '#/Txhvk1Tl/'].join('\n'),
  163.       tripcode: ['# Filter any tripfags', '#/^!/'].join('\n'),
  164.       mod: ['# Set a custom class for mods:', '#/Mod$/;highlight:mod;op:yes', '# Set a custom class for moot:', '#/Admin$/;highlight:moot;op:yes'].join('\n'),
  165.       email: ['# Filter any e-mails that are not `sage` on /a/ and /jp/:', '#/^(?!sage$)/;boards:a,jp'].join('\n'),
  166.       subject: ['# Filter Generals on /v/:', '#/general/i;boards:v;op:only'].join('\n'),
  167.       comment: ['# Filter Stallman copypasta on /g/:', '#/what you\'re refer+ing to as linux/i;boards:g'].join('\n'),
  168.       country: [''].join('\n'),
  169.       filename: [''].join('\n'),
  170.       dimensions: ['# Highlight potential wallpapers:', '#/1920x1080/;op:yes;highlight;top:no;boards:w,wg'].join('\n'),
  171.       filesize: [''].join('\n'),
  172.       md5: [''].join('\n')
  173.     },
  174.     sauces: ['http://iqdb.org/?url=$1', 'http://www.google.com/searchbyimage?image_url=$1', '#http://tineye.com/search?url=$1', '#http://saucenao.com/search.php?db=999&url=$1', '#http://3d.iqdb.org/?url=$1', '#http://regex.info/exif.cgi?imgurl=$2', '# uploaders:', '#http://imgur.com/upload?url=$2;text:Upload to imgur', '#http://omploader.org/upload?url1=$2;text:Upload to omploader', '# "View Same" in archives:', '#http://archive.foolz.us/_/search/image/$3/;text:View same on foolz', '#http://archive.foolz.us/$4/search/image/$3/;text:View same on foolz /$4/', '#https://archive.installgentoo.net/$4/image/$3;text:View same on installgentoo /$4/'].join('\n'),
  175.     time: '%m/%d/%y(%a)%H:%M',
  176.     backlink: '>>%id',
  177.     fileInfo: '%l (%p%s, %r)',
  178.     favicon: 'ferongr',
  179.     hotkeys: {
  180.       openQR: ['i', 'Open QR with post number inserted'],
  181.       openEmptyQR: ['I', 'Open QR without post number inserted'],
  182.       openOptions: ['ctrl+o', 'Open Options'],
  183.       close: ['Esc', 'Close Options or QR'],
  184.       spoiler: ['ctrl+s', 'Quick spoiler tags'],
  185.       code: ['alt+c', 'Quick code tags'],
  186.       submit: ['alt+s', 'Submit post'],
  187.       watch: ['w', 'Watch thread'],
  188.       update: ['u', 'Update now'],
  189.       unreadCountTo0: ['z', 'Mark thread as read'],
  190.       expandImage: ['m', 'Expand selected image'],
  191.       expandAllImages: ['M', 'Expand all images'],
  192.       zero: ['0', 'Jump to page 0'],
  193.       nextPage: ['L', 'Jump to the next page'],
  194.       previousPage: ['H', 'Jump to the previous page'],
  195.       nextThread: ['n', 'See next thread'],
  196.       previousThread: ['p', 'See previous thread'],
  197.       expandThread: ['e', 'Expand thread'],
  198.       openThreadTab: ['o', 'Open thread in current tab'],
  199.       openThread: ['O', 'Open thread in new tab'],
  200.       nextReply: ['J', 'Select next reply'],
  201.       previousReply: ['K', 'Select previous reply'],
  202.       hide: ['x', 'Hide thread']
  203.     },
  204.     updater: {
  205.       checkbox: {
  206.         'Beep': [false, 'Beep on new post to completely read thread'],
  207.         'Scrolling': [false, 'Scroll updated posts into view. Only enabled at bottom of page.'],
  208.         'Scroll BG': [false, 'Scroll background tabs'],
  209.         'Verbose': [true, 'Show countdown timer, new post count'],
  210.         'Auto Update': [true, 'Automatically fetch new posts']
  211.       },
  212.       'Interval': 30
  213.     }
  214.   };
  215.  
  216.   Conf = {};
  217.  
  218.   d = document;
  219.  
  220.   g = {};
  221.  
  222.   UI = {
  223.     dialog: function(id, position, html) {
  224.       var el;
  225.       el = d.createElement('div');
  226.       el.className = 'reply dialog';
  227.       el.innerHTML = html;
  228.       el.id = id;
  229.       el.style.cssText = localStorage.getItem("" + Main.namespace + id + ".position") || position;
  230.       el.querySelector('.move').addEventListener('mousedown', UI.dragstart, false);
  231.       return el;
  232.     },
  233.     dragstart: function(e) {
  234.       var el, rect;
  235.       e.preventDefault();
  236.       UI.el = el = this.parentNode;
  237.       d.addEventListener('mousemove', UI.drag, false);
  238.       d.addEventListener('mouseup', UI.dragend, false);
  239.       rect = el.getBoundingClientRect();
  240.       UI.dx = e.clientX - rect.left;
  241.       UI.dy = e.clientY - rect.top;
  242.       UI.width = d.documentElement.clientWidth - rect.width;
  243.       return UI.height = d.documentElement.clientHeight - rect.height;
  244.     },
  245.     drag: function(e) {
  246.       var left, style, top;
  247.       left = e.clientX - UI.dx;
  248.       top = e.clientY - UI.dy;
  249.       left = left < 10 ? '0px' : UI.width - left < 10 ? null : left + 'px';
  250.       top = top < 10 ? '0px' : UI.height - top < 10 ? null : top + 'px';
  251.       style = UI.el.style;
  252.       style.left = left;
  253.       style.top = top;
  254.       style.right = left === null ? '0px' : null;
  255.       return style.bottom = top === null ? '0px' : null;
  256.     },
  257.     dragend: function() {
  258.       localStorage.setItem("" + Main.namespace + UI.el.id + ".position", UI.el.style.cssText);
  259.       d.removeEventListener('mousemove', UI.drag, false);
  260.       d.removeEventListener('mouseup', UI.dragend, false);
  261.       return delete UI.el;
  262.     },
  263.     hover: function(e) {
  264.       var clientHeight, clientWidth, clientX, clientY, height, style, top, _ref;
  265.       clientX = e.clientX, clientY = e.clientY;
  266.       style = UI.el.style;
  267.       _ref = d.documentElement, clientHeight = _ref.clientHeight, clientWidth = _ref.clientWidth;
  268.       height = UI.el.offsetHeight;
  269.       top = clientY - 120;
  270.       style.top = clientHeight <= height || top <= 0 ? '0px' : top + height >= clientHeight ? clientHeight - height + 'px' : top + 'px';
  271.       if (clientX <= clientWidth - 400) {
  272.         style.left = clientX + 45 + 'px';
  273.         return style.right = null;
  274.       } else {
  275.         style.left = null;
  276.         return style.right = clientWidth - clientX + 45 + 'px';
  277.       }
  278.     },
  279.     hoverend: function() {
  280.       $.rm(UI.el);
  281.       return delete UI.el;
  282.     }
  283.   };
  284.  
  285.   /*
  286.   loosely follows the jquery api:
  287.   http://api.jquery.com/
  288.   not chainable
  289.   */
  290.  
  291.  
  292.   $ = function(selector, root) {
  293.     if (root == null) {
  294.       root = d.body;
  295.     }
  296.     return root.querySelector(selector);
  297.   };
  298.  
  299.   $.extend = function(object, properties) {
  300.     var key, val;
  301.     for (key in properties) {
  302.       val = properties[key];
  303.       object[key] = val;
  304.     }
  305.   };
  306.  
  307.   $.extend($, {
  308.     SECOND: 1000,
  309.     MINUTE: 1000 * 60,
  310.     HOUR: 1000 * 60 * 60,
  311.     DAY: 1000 * 60 * 60 * 24,
  312.     log: typeof (_base = console.log).bind === "function" ? _base.bind(console) : void 0,
  313.     engine: /WebKit|Presto|Gecko/.exec(navigator.userAgent)[0].toLowerCase(),
  314.     ready: function(fc) {
  315.       var cb;
  316.       if (/interactive|complete/.test(d.readyState)) {
  317.         return setTimeout(fc);
  318.       }
  319.       cb = function() {
  320.         $.off(d, 'DOMContentLoaded', cb);
  321.         return fc();
  322.       };
  323.       return $.on(d, 'DOMContentLoaded', cb);
  324.     },
  325.     sync: function(key, cb) {
  326.       key = Main.namespace + key;
  327.       return $.on(window, 'storage', function(e) {
  328.         if (e.key === key) {
  329.           return cb(JSON.parse(e.newValue));
  330.         }
  331.       });
  332.     },
  333.     id: function(id) {
  334.       return d.getElementById(id);
  335.     },
  336.     formData: function(arg) {
  337.       var fd, key, val;
  338.       if (arg instanceof HTMLFormElement) {
  339.         fd = new FormData(arg);
  340.       } else {
  341.         fd = new FormData();
  342.         for (key in arg) {
  343.           val = arg[key];
  344.           if (val) {
  345.             fd.append(key, val);
  346.           }
  347.         }
  348.       }
  349.       return fd;
  350.     },
  351.     ajax: function(url, callbacks, opts) {
  352.       var form, headers, key, r, type, upCallbacks, val;
  353.       if (opts == null) {
  354.         opts = {};
  355.       }
  356.       type = opts.type, headers = opts.headers, upCallbacks = opts.upCallbacks, form = opts.form;
  357.       r = new XMLHttpRequest();
  358.       type || (type = form && 'post' || 'get');
  359.       r.open(type, url, true);
  360.       for (key in headers) {
  361.         val = headers[key];
  362.         r.setRequestHeader(key, val);
  363.       }
  364.       $.extend(r, callbacks);
  365.       $.extend(r.upload, upCallbacks);
  366.       if (type === 'post') {
  367.         r.withCredentials = true;
  368.       }
  369.       r.send(form);
  370.       return r;
  371.     },
  372.     cache: function(url, cb) {
  373.       var req;
  374.       if (req = $.cache.requests[url]) {
  375.         if (req.readyState === 4) {
  376.           return cb.call(req);
  377.         } else {
  378.           return req.callbacks.push(cb);
  379.         }
  380.       } else {
  381.         req = $.ajax(url, {
  382.           onload: function() {
  383.             var _i, _len, _ref, _results;
  384.             _ref = this.callbacks;
  385.             _results = [];
  386.             for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  387.               cb = _ref[_i];
  388.               _results.push(cb.call(this));
  389.             }
  390.             return _results;
  391.           },
  392.           onabort: function() {
  393.             return delete $.cache.requests[url];
  394.           },
  395.           onerror: function() {
  396.             return delete $.cache.requests[url];
  397.           }
  398.         });
  399.         req.callbacks = [cb];
  400.         return $.cache.requests[url] = req;
  401.       }
  402.     },
  403.     cb: {
  404.       checked: function() {
  405.         $.set(this.name, this.checked);
  406.         return Conf[this.name] = this.checked;
  407.       },
  408.       value: function() {
  409.         $.set(this.name, this.value.trim());
  410.         return Conf[this.name] = this.value;
  411.       }
  412.     },
  413.     addStyle: function(css) {
  414.       var f, style;
  415.       style = $.el('style', {
  416.         textContent: css
  417.       });
  418.       f = function() {
  419.         var root;
  420.         if (root = d.head || d.documentElement) {
  421.           return $.add(root, style);
  422.         } else {
  423.           return setTimeout(f, 20);
  424.         }
  425.       };
  426.       f();
  427.       return style;
  428.     },
  429.     x: function(path, root) {
  430.       if (root == null) {
  431.         root = d.body;
  432.       }
  433.       return d.evaluate(path, root, null, 8, null).singleNodeValue;
  434.     },
  435.     addClass: function(el, className) {
  436.       return el.classList.add(className);
  437.     },
  438.     rmClass: function(el, className) {
  439.       return el.classList.remove(className);
  440.     },
  441.     rm: function(el) {
  442.       return el.parentNode.removeChild(el);
  443.     },
  444.     tn: function(s) {
  445.       return d.createTextNode(s);
  446.     },
  447.     nodes: function(nodes) {
  448.       var frag, node, _i, _len;
  449.       if (!(nodes instanceof Array)) {
  450.         return nodes;
  451.       }
  452.       frag = d.createDocumentFragment();
  453.       for (_i = 0, _len = nodes.length; _i < _len; _i++) {
  454.         node = nodes[_i];
  455.         frag.appendChild(node);
  456.       }
  457.       return frag;
  458.     },
  459.     add: function(parent, children) {
  460.       return parent.appendChild($.nodes(children));
  461.     },
  462.     prepend: function(parent, children) {
  463.       return parent.insertBefore($.nodes(children), parent.firstChild);
  464.     },
  465.     after: function(root, el) {
  466.       return root.parentNode.insertBefore($.nodes(el), root.nextSibling);
  467.     },
  468.     before: function(root, el) {
  469.       return root.parentNode.insertBefore($.nodes(el), root);
  470.     },
  471.     replace: function(root, el) {
  472.       return root.parentNode.replaceChild($.nodes(el), root);
  473.     },
  474.     el: function(tag, properties) {
  475.       var el;
  476.       el = d.createElement(tag);
  477.       if (properties) {
  478.         $.extend(el, properties);
  479.       }
  480.       return el;
  481.     },
  482.     on: function(el, events, handler) {
  483.       var event, _i, _len, _ref;
  484.       _ref = events.split(' ');
  485.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  486.         event = _ref[_i];
  487.         el.addEventListener(event, handler, false);
  488.       }
  489.     },
  490.     off: function(el, events, handler) {
  491.       var event, _i, _len, _ref;
  492.       _ref = events.split(' ');
  493.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  494.         event = _ref[_i];
  495.         el.removeEventListener(event, handler, false);
  496.       }
  497.     },
  498.     open: function(url) {
  499.       return (GM_openInTab || window.open)(location.protocol + url, '_blank');
  500.     },
  501.     event: function(el, e) {
  502.       return el.dispatchEvent(e);
  503.     },
  504.     globalEval: function(code) {
  505.       var script;
  506.       script = $.el('script', {
  507.         textContent: code
  508.       });
  509.       $.add(d.head, script);
  510.       return $.rm(script);
  511.     },
  512.     bytesToString: function(size) {
  513.       var unit;
  514.       unit = 0;
  515.       while (size >= 1024) {
  516.         size /= 1024;
  517.         unit++;
  518.       }
  519.       size = unit > 1 ? Math.round(size * 100) / 100 : Math.round(size);
  520.       return "" + size + " " + ['B', 'KB', 'MB', 'GB'][unit];
  521.     },
  522.     debounce: function(wait, fn) {
  523.       var timeout;
  524.       timeout = null;
  525.       return function() {
  526.         if (timeout) {
  527.           clearTimeout(timeout);
  528.         } else {
  529.           fn.apply(this, arguments);
  530.         }
  531.         return timeout = setTimeout((function() {
  532.           return timeout = null;
  533.         }), wait);
  534.       };
  535.     },
  536.     visible: function(el) {
  537.       var rect = el.getBoundingClientRect();
  538.       return (
  539.         rect.top + rect.height >= 0 &&
  540.         ( d.documentElement.clientHeight - rect.bottom ) + rect.height >= 0
  541.       );
  542.     }
  543.   });
  544.  
  545.   $.cache.requests = {};
  546.  
  547.   $.extend($, typeof GM_deleteValue !== "undefined" && GM_deleteValue !== null ? {
  548.     "delete": function(name) {
  549.       name = Main.namespace + name;
  550.       return GM_deleteValue(name);
  551.     },
  552.     get: function(name, defaultValue) {
  553.       var value;
  554.       name = Main.namespace + name;
  555.       if (value = GM_getValue(name)) {
  556.         return JSON.parse(value);
  557.       } else {
  558.         return defaultValue;
  559.       }
  560.     },
  561.     set: function(name, value) {
  562.       name = Main.namespace + name;
  563.       localStorage.setItem(name, JSON.stringify(value));
  564.       return GM_setValue(name, JSON.stringify(value));
  565.     }
  566.   } : {
  567.     "delete": function(name) {
  568.       return localStorage.removeItem(Main.namespace + name);
  569.     },
  570.     get: function(name, defaultValue) {
  571.       var value;
  572.       if (value = localStorage.getItem(Main.namespace + name)) {
  573.         return JSON.parse(value);
  574.       } else {
  575.         return defaultValue;
  576.       }
  577.     },
  578.     set: function(name, value) {
  579.       return localStorage.setItem(Main.namespace + name, JSON.stringify(value));
  580.     }
  581.   });
  582.  
  583.   $$ = function(selector, root) {
  584.     if (root == null) {
  585.       root = d.body;
  586.     }
  587.     return Array.prototype.slice.call(root.querySelectorAll(selector));
  588.   };
  589.  
  590.   Filter = {
  591.     filters: {},
  592.     init: function() {
  593.       var boards, err, filter, hl, key, op, regexp, stub, top, _i, _len, _ref, _ref1, _ref2, _ref3, _ref4;
  594.       for (key in Config.filter) {
  595.         this.filters[key] = [];
  596.         _ref = Conf[key].split('\n');
  597.         for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  598.           filter = _ref[_i];
  599.           if (filter[0] === '#') {
  600.             continue;
  601.           }
  602.           if (!(regexp = filter.match(/\/(.+)\/(\w*)/))) {
  603.             continue;
  604.           }
  605.           filter = filter.replace(regexp[0], '');
  606.           boards = ((_ref1 = filter.match(/boards:([^;]+)/)) != null ? _ref1[1].toLowerCase() : void 0) || 'global';
  607.           if (boards !== 'global' && boards.split(',').indexOf(g.BOARD) === -1) {
  608.             continue;
  609.           }
  610.           if (key === 'md5') {
  611.             regexp = regexp[1];
  612.           } else {
  613.             try {
  614.               regexp = RegExp(regexp[1], regexp[2]);
  615.             } catch (_error) {
  616.               err = _error;
  617.               alert(err.message);
  618.               continue;
  619.             }
  620.           }
  621.           op = ((_ref2 = filter.match(/[^t]op:(yes|no|only)/)) != null ? _ref2[1] : void 0) || 'no';
  622.           stub = (function() {
  623.             var _ref3;
  624.             switch ((_ref3 = filter.match(/stub:(yes|no)/)) != null ? _ref3[1] : void 0) {
  625.               case 'yes':
  626.                 return true;
  627.               case 'no':
  628.                 return false;
  629.               default:
  630.                 return Conf['Show Stubs'];
  631.             }
  632.           })();
  633.           if (hl = /highlight/.test(filter)) {
  634.             hl = ((_ref3 = filter.match(/highlight:(\w+)/)) != null ? _ref3[1] : void 0) || 'filter_highlight';
  635.             top = ((_ref4 = filter.match(/top:(yes|no)/)) != null ? _ref4[1] : void 0) || 'yes';
  636.             top = top === 'yes';
  637.           }
  638.           this.filters[key].push(this.createFilter(regexp, op, stub, hl, top));
  639.         }
  640.         if (!this.filters[key].length) {
  641.           delete this.filters[key];
  642.         }
  643.       }
  644.       if (Object.keys(this.filters).length) {
  645.         return Main.callbacks.push(this.node);
  646.       }
  647.     },
  648.     createFilter: function(regexp, op, stub, hl, top) {
  649.       var settings, test;
  650.       test = typeof regexp === 'string' ? function(value) {
  651.         return regexp === value;
  652.       } : function(value) {
  653.         return regexp.test(value);
  654.       };
  655.       settings = {
  656.         hide: !hl,
  657.         stub: stub,
  658.         "class": hl,
  659.         top: top
  660.       };
  661.       return function(value, isOP) {
  662.         if (isOP && op === 'no' || !isOP && op === 'only') {
  663.           return false;
  664.         }
  665.         if (!test(value)) {
  666.           return false;
  667.         }
  668.         return settings;
  669.       };
  670.     },
  671.     node: function(post) {
  672.       var filter, firstThread, isOP, key, result, root, thisThread, value, _i, _len, _ref;
  673.       if (post.isInlined) {
  674.         return;
  675.       }
  676.       isOP = post.ID === post.threadID;
  677.       root = post.root;
  678.       for (key in Filter.filters) {
  679.         value = Filter[key](post);
  680.         if (value === false) {
  681.           continue;
  682.         }
  683.         _ref = Filter.filters[key];
  684.         for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  685.           filter = _ref[_i];
  686.           if (!(result = filter(value, isOP))) {
  687.             continue;
  688.           }
  689.           if (result.hide) {
  690.             if (isOP) {
  691.               if (!g.REPLY) {
  692.                 ThreadHiding.hide(root.parentNode, result.stub);
  693.               } else {
  694.                 continue;
  695.               }
  696.             } else {
  697.               ReplyHiding.hide(root, result.stub);
  698.             }
  699.             return;
  700.           }
  701.           $.addClass(root, result["class"]);
  702.           if (isOP && result.top && !g.REPLY) {
  703.             thisThread = root.parentNode;
  704.             if (firstThread = $('div[class="postContainer opContainer"]')) {
  705.               if (firstThread !== root) {
  706.                 $.before(firstThread.parentNode, [thisThread, thisThread.nextElementSibling]);
  707.               }
  708.             }
  709.           }
  710.         }
  711.       }
  712.     },
  713.     name: function(post) {
  714.       return $('.name', post.el).textContent;
  715.     },
  716.     uniqueid: function(post) {
  717.       var uid;
  718.       if (uid = $('.posteruid', post.el)) {
  719.         return uid.textContent.slice(5, -1);
  720.       }
  721.       return false;
  722.     },
  723.     tripcode: function(post) {
  724.       var trip;
  725.       if (trip = $('.postertrip', post.el)) {
  726.         return trip.textContent;
  727.       }
  728.       return false;
  729.     },
  730.     mod: function(post) {
  731.       var mod;
  732.       if (mod = $('.capcode', post.el)) {
  733.         return mod.textContent;
  734.       }
  735.       return false;
  736.     },
  737.     email: function(post) {
  738.       var mail;
  739.       if (mail = $('.useremail', post.el)) {
  740.         return decodeURIComponent(mail.href.slice(7));
  741.       }
  742.       return false;
  743.     },
  744.     subject: function(post) {
  745.       var subject;
  746.       if (subject = $('.postInfo .subject', post.el)) {
  747.         return subject.textContent;
  748.       }
  749.       return false;
  750.     },
  751.     comment: function(post) {
  752.       var data, i, nodes, text, _i, _ref;
  753.       text = [];
  754.       nodes = d.evaluate('.//br|.//text()', post.blockquote, null, 7, null);
  755.       for (i = _i = 0, _ref = nodes.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
  756.         text.push((data = nodes.snapshotItem(i).data) ? data : '\n');
  757.       }
  758.       return text.join('');
  759.     },
  760.     country: function(post) {
  761.       var flag;
  762.       if (flag = $('.countryFlag', post.el)) {
  763.         return flag.title;
  764.       }
  765.       return false;
  766.     },
  767.     filename: function(post) {
  768.       var file, fileInfo;
  769.       fileInfo = post.fileInfo;
  770.       if (fileInfo) {
  771.         if (file = $('.fileText > span', fileInfo)) {
  772.           return file.title || file.innerHTML;
  773.         } else {
  774.           return fileInfo.firstElementChild.dataset.filename;
  775.         }
  776.       }
  777.       return false;
  778.     },
  779.     dimensions: function(post) {
  780.       var fileInfo, match;
  781.       fileInfo = post.fileInfo;
  782.       if (fileInfo && (match = fileInfo.textContent.match(/\d+x\d+/))) {
  783.         return match[0];
  784.       }
  785.       return false;
  786.     },
  787.     filesize: function(post) {
  788.       var img;
  789.       img = post.img;
  790.       if (img) {
  791.         return img.alt.replace('Spoiler Image, ', '');
  792.       }
  793.       return false;
  794.     },
  795.     md5: function(post) {
  796.       var img;
  797.       img = post.img;
  798.       if (img) {
  799.         return img.dataset.md5;
  800.       }
  801.       return false;
  802.     },
  803.     menuInit: function() {
  804.       var div, entry, type, _i, _len, _ref;
  805.       div = $.el('div', {
  806.         textContent: 'Filter'
  807.       });
  808.       entry = {
  809.         el: div,
  810.         open: function() {
  811.           return true;
  812.         },
  813.         children: []
  814.       };
  815.       _ref = [['Name', 'name'], ['Unique ID', 'uniqueid'], ['Tripcode', 'tripcode'], ['Admin/Mod', 'mod'], ['E-mail', 'email'], ['Subject', 'subject'], ['Comment', 'comment'], ['Country', 'country'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'md5']];
  816.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  817.         type = _ref[_i];
  818.         entry.children.push(Filter.createSubEntry(type[0], type[1]));
  819.       }
  820.       return Menu.addEntry(entry);
  821.     },
  822.     createSubEntry: function(text, type) {
  823.       var el, onclick, open;
  824.       el = $.el('a', {
  825.         href: 'javascript:;',
  826.         textContent: text
  827.       });
  828.       onclick = null;
  829.       open = function(post) {
  830.         var value;
  831.         value = Filter[type](post);
  832.         if (value === false) {
  833.           return false;
  834.         }
  835.         $.off(el, 'click', onclick);
  836.         onclick = function() {
  837.           var re, save, select, ta, tl;
  838.           re = type === 'md5' ? value : value.replace(/\/|\\|\^|\$|\n|\.|\(|\)|\{|\}|\[|\]|\?|\*|\+|\|/g, function(c) {
  839.             if (c === '\n') {
  840.               return '\\n';
  841.             } else if (c === '\\') {
  842.               return '\\\\';
  843.             } else {
  844.               return "\\" + c;
  845.             }
  846.           });
  847.           re = type === 'md5' ? "/" + value + "/" : "/^" + re + "$/";
  848.           if (/\bop\b/.test(post["class"])) {
  849.             re += ';op:yes';
  850.           }
  851.           save = (save = $.get(type, '')) ? "" + save + "\n" + re : re;
  852.           $.set(type, save);
  853.           Options.dialog();
  854.           select = $('select[name=filter]', $.id('options'));
  855.           select.value = type;
  856.           $.event(select, new Event('change'));
  857.           $.id('filter_tab').checked = true;
  858.           ta = select.nextElementSibling;
  859.           tl = ta.textLength;
  860.           ta.setSelectionRange(tl, tl);
  861.           return ta.focus();
  862.         };
  863.         $.on(el, 'click', onclick);
  864.         return true;
  865.       };
  866.       return {
  867.         el: el,
  868.         open: open
  869.       };
  870.     }
  871.   };
  872.  
  873.   StrikethroughQuotes = {
  874.     init: function() {
  875.       return Main.callbacks.push(this.node);
  876.     },
  877.     node: function(post) {
  878.       var el, quote, show_stub, _i, _len, _ref;
  879.       if (post.isInlined) {
  880.         return;
  881.       }
  882.       _ref = post.quotes;
  883.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  884.         quote = _ref[_i];
  885.         if (!((el = $.id(quote.hash.slice(1))) && quote.hostname === 'boards.4chan.org' && !/catalog$/.test(quote.pathname) && el.hidden)) {
  886.           continue;
  887.         }
  888.         $.addClass(quote, 'filtered');
  889.         if (Conf['Recursive Filtering'] && post.ID !== post.threadID) {
  890.           show_stub = !!$.x('preceding-sibling::div[contains(@class,"stub")]', el);
  891.           ReplyHiding.hide(post.root, show_stub);
  892.         }
  893.       }
  894.     }
  895.   };
  896.  
  897.   ExpandComment = {
  898.     init: function() {
  899.       var a, _i, _len, _ref;
  900.       _ref = $$('.abbr');
  901.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  902.         a = _ref[_i];
  903.         $.on(a.firstElementChild, 'click', ExpandComment.expand);
  904.       }
  905.     },
  906.     expand: function(e) {
  907.       var a, replyID, threadID, _, _ref;
  908.       e.preventDefault();
  909.       _ref = this.href.match(/(\d+)#p(\d+)/), _ = _ref[0], threadID = _ref[1], replyID = _ref[2];
  910.       this.textContent = "Loading No." + replyID + "...";
  911.       a = this;
  912.       return $.cache("//a.4cdn.org" + this.pathname + ".json", function() {
  913.         return ExpandComment.parse(this, a, threadID, replyID);
  914.       });
  915.     },
  916.     parse: function(req, a, threadID, replyID) {
  917.       var bq, clone, href, post, posts, quote, quotes, spoilerRange, _i, _j, _len, _len1;
  918.       if (req.status !== 200) {
  919.         a.textContent = "" + req.status + " " + req.statusText;
  920.         return;
  921.       }
  922.       posts = JSON.parse(req.response).posts;
  923.       if (spoilerRange = posts[0].custom_spoiler) {
  924.         Build.spoilerRange[g.BOARD] = spoilerRange;
  925.       }
  926.       replyID = +replyID;
  927.       for (_i = 0, _len = posts.length; _i < _len; _i++) {
  928.         post = posts[_i];
  929.         if (post.no === replyID) {
  930.           break;
  931.         }
  932.       }
  933.       if (post.no !== replyID) {
  934.         a.textContent = 'No.#{replyID} not found.';
  935.         return;
  936.       }
  937.       bq = $.id("m" + replyID);
  938.       clone = bq.cloneNode(false);
  939. //      clone.innerHTML = post.com.replace(/\b(\&gt;\s*)?([A-Za-z]+)-\2/ig, "$1$2");
  940.       quotes = clone.getElementsByClassName('quotelink');
  941.       for (_j = 0, _len1 = quotes.length; _j < _len1; _j++) {
  942.         quote = quotes[_j];
  943.         href = quote.getAttribute('href');
  944.         if (href[0] === '/') {
  945.           continue;
  946.         }
  947.         quote.href = "thread/" + href;
  948.       }
  949.       post = {
  950.         blockquote: clone,
  951.         threadID: threadID,
  952.         quotes: quotes,
  953.         backlinks: []
  954.       };
  955.       if (Conf['Resurrect Quotes']) {
  956.         Quotify.node(post);
  957.       }
  958.       if (Conf['Quote Preview']) {
  959.         QuotePreview.node(post);
  960.       }
  961.       if (Conf['Quote Inline']) {
  962.         QuoteInline.node(post);
  963.       }
  964.       if (Conf['Indicate OP quote']) {
  965.         QuoteOP.node(post);
  966.       }
  967.       if (Conf['Indicate Cross-thread Quotes']) {
  968.         QuoteCT.node(post);
  969.       }
  970.       $.replace(bq, clone);
  971.       return Main.prettify(clone);
  972.     }
  973.   };
  974.  
  975.   ExpandThread = {
  976.     init: function() {
  977.       var a, span, _i, _len, _ref, _results;
  978.       _ref = $$('.summary');
  979.       _results = [];
  980.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  981.         span = _ref[_i];
  982.         a = $.el('a', {
  983.           textContent: "+ " + span.textContent,
  984.           className: 'summary desktop',
  985.           href: 'javascript:;'
  986.         });
  987.         $.on(a, 'click', function() {
  988.           return ExpandThread.toggle(this.parentNode);
  989.         });
  990.         _results.push($.replace(span, a));
  991.       }
  992.       return _results;
  993.     },
  994.     toggle: function(thread) {
  995.       var a, num, replies, reply, url, _i, _len;
  996.       url = "//a.4cdn.org/" + g.BOARD + "/thread/" + thread.id.slice(1) + ".json";
  997.       a = $('.summary', thread);
  998.       switch (a.textContent[0]) {
  999.         case '+':
  1000.           a.textContent = a.textContent.replace('+', '× Loading...');
  1001.           $.cache(url, function() {
  1002.             return ExpandThread.parse(this, thread, a);
  1003.           });
  1004.           break;
  1005.         case '×':
  1006.           a.textContent = a.textContent.replace('× Loading...', '+');
  1007.           $.cache.requests[url].abort();
  1008.           break;
  1009.         case '-':
  1010.           a.textContent = a.textContent.replace('-', '+');
  1011.           num = (function() {
  1012.             switch (g.BOARD) {
  1013.               case 'b':
  1014.               case 'vg':
  1015.                 return 3;
  1016.               case 't':
  1017.                 return 1;
  1018.               default:
  1019.                 return 5;
  1020.             }
  1021.           })();
  1022.           replies = $$('.replyContainer', thread);
  1023.           replies.splice(replies.length - num, num);
  1024.           for (_i = 0, _len = replies.length; _i < _len; _i++) {
  1025.             reply = replies[_i];
  1026.             $.rm(reply);
  1027.           }
  1028.       }
  1029.     },
  1030.     parse: function(req, thread, a) {
  1031.       var backlink, id, link, nodes, post, posts, replies, reply, spoilerRange, threadID, _i, _j, _k, _len, _len1, _len2, _ref, _ref1;
  1032.       if (req.status !== 200) {
  1033.         a.textContent = "" + req.status + " " + req.statusText;
  1034.         $.off(a, 'click', ExpandThread.cb.toggle);
  1035.         return;
  1036.       }
  1037.       a.textContent = a.textContent.replace('× Loading...', '-');
  1038.       posts = JSON.parse(req.response).posts;
  1039.       if (spoilerRange = posts[0].custom_spoiler) {
  1040.         Build.spoilerRange[g.BOARD] = spoilerRange;
  1041.       }
  1042.       replies = posts.slice(1);
  1043.       threadID = thread.id.slice(1);
  1044.       nodes = [];
  1045.       for (_i = 0, _len = replies.length; _i < _len; _i++) {
  1046.         reply = replies[_i];
  1047.         post = Build.postFromObject(reply, g.BOARD);
  1048.         id = reply.no;
  1049.         link = $('a[title="Highlight this post"]', post);
  1050.         link.href = "thread/" + threadID + "#p" + id;
  1051.         link.nextSibling.href = "thread/" + threadID + "#q" + id;
  1052.         nodes.push(post);
  1053.       }
  1054.       _ref = $$('.summary ~ .replyContainer', a.parentNode);
  1055.       for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
  1056.         post = _ref[_j];
  1057.         $.rm(post);
  1058.       }
  1059.       _ref1 = $$('.backlink', a.previousElementSibling);
  1060.       for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
  1061.         backlink = _ref1[_k];
  1062.         if (!$.id(backlink.hash.slice(1))) {
  1063.           $.rm(backlink);
  1064.         }
  1065.       }
  1066.       return $.after(a, nodes);
  1067.     }
  1068.   };
  1069.  
  1070.   ThreadHiding = {
  1071.     init: function() {
  1072.       var a, hiddenThreads, thread, _i, _len, _ref;
  1073.       hiddenThreads = ThreadHiding.sync();
  1074.       if (g.CATALOG) {
  1075.         return;
  1076.       }
  1077.       _ref = $$('.thread');
  1078.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1079.         thread = _ref[_i];
  1080.         a = $.el('a', {
  1081.           className: 'hide_thread_button',
  1082.           innerHTML: '<span>[ - ]</span>',
  1083.           href: 'javascript:;'
  1084.         });
  1085.         $.on(a, 'click', ThreadHiding.cb);
  1086.         $.prepend(thread, a);
  1087.         if (thread.id.slice(1) in hiddenThreads) {
  1088.           ThreadHiding.hide(thread);
  1089.         }
  1090.       }
  1091.     },
  1092.     sync: function() {
  1093.       var hiddenThreads, hiddenThreadsCatalog, id;
  1094.       hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {});
  1095.       hiddenThreadsCatalog = JSON.parse(localStorage.getItem("4chan-hide-t-" + g.BOARD)) || {};
  1096.       if (g.CATALOG) {
  1097.         for (id in hiddenThreads) {
  1098.           hiddenThreadsCatalog[id] = true;
  1099.         }
  1100.         localStorage.setItem("4chan-hide-t-" + g.BOARD, JSON.stringify(hiddenThreadsCatalog));
  1101.       } else {
  1102.         for (id in hiddenThreadsCatalog) {
  1103.           if (!(id in hiddenThreads)) {
  1104.             hiddenThreads[id] = Date.now();
  1105.           }
  1106.         }
  1107.         $.set("hiddenThreads/" + g.BOARD + "/", hiddenThreads);
  1108.       }
  1109.       return hiddenThreads;
  1110.     },
  1111.     cb: function() {
  1112.       return ThreadHiding.toggle($.x('ancestor::div[parent::div[@class="board"]]', this));
  1113.     },
  1114.     toggle: function(thread) {
  1115.       var hiddenThreads, id;
  1116.       hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {});
  1117.       id = thread.id.slice(1);
  1118.       if (thread.hidden || /\bhidden_thread\b/.test(thread.firstChild.className)) {
  1119.         ThreadHiding.show(thread);
  1120.         delete hiddenThreads[id];
  1121.       } else {
  1122.         ThreadHiding.hide(thread);
  1123.         hiddenThreads[id] = Date.now();
  1124.       }
  1125.       return $.set("hiddenThreads/" + g.BOARD + "/", hiddenThreads);
  1126.     },
  1127.     hide: function(thread, show_stub) {
  1128.       var a, menuButton, num, opInfo, span, stub, text;
  1129.       if (show_stub == null) {
  1130.         show_stub = Conf['Show Stubs'];
  1131.       }
  1132.       if (!show_stub) {
  1133.         thread.hidden = true;
  1134.         thread.nextElementSibling.hidden = true;
  1135.         return;
  1136.       }
  1137.       if (/\bhidden_thread\b/.test(thread.firstChild.className)) {
  1138.         return;
  1139.       }
  1140.       num = 0;
  1141.       if (span = $('.summary', thread)) {
  1142.         num = Number(span.textContent.match(/\d+/));
  1143.       }
  1144.       num += $$('.opContainer ~ .replyContainer', thread).length;
  1145.       text = num === 1 ? '1 reply' : "" + num + " replies";
  1146.       opInfo = $('.desktop > .nameBlock', thread).textContent;
  1147.       stub = $.el('div', {
  1148.         className: 'hide_thread_button hidden_thread',
  1149.         innerHTML: '<a href="javascript:;"><span>[ + ]</span> </a>'
  1150.       });
  1151.       a = stub.firstChild;
  1152.       $.on(a, 'click', ThreadHiding.cb);
  1153.       $.add(a, $.tn("" + opInfo + " (" + text + ")"));
  1154.       if (Conf['Menu']) {
  1155.         menuButton = Menu.a.cloneNode(true);
  1156.         $.on(menuButton, 'click', Menu.toggle);
  1157.         $.add(stub, [$.tn(' '), menuButton]);
  1158.       }
  1159.       return $.prepend(thread, stub);
  1160.     },
  1161.     show: function(thread) {
  1162.       var stub;
  1163.       if (stub = $('.hidden_thread', thread)) {
  1164.         $.rm(stub);
  1165.       }
  1166.       thread.hidden = false;
  1167.       return thread.nextElementSibling.hidden = false;
  1168.     }
  1169.   };
  1170.  
  1171.   ReplyHiding = {
  1172.     init: function() {
  1173.       return Main.callbacks.push(this.node);
  1174.     },
  1175.     node: function(post) {
  1176.       var side;
  1177.       if (post.isInlined || post.ID === post.threadID) {
  1178.         return;
  1179.       }
  1180.       side = $('.sideArrows', post.root);
  1181.       $.addClass(side, 'hide_reply_button');
  1182.       side.innerHTML = '<a href="javascript:;"><span>[ - ]</span></a>';
  1183.       $.on(side.firstChild, 'click', ReplyHiding.toggle);
  1184.       if (post.ID in g.hiddenReplies) {
  1185.         return ReplyHiding.hide(post.root);
  1186.       }
  1187.     },
  1188.     toggle: function() {
  1189.       var button, id, quote, quotes, root, _i, _j, _len, _len1;
  1190.       button = this.parentNode;
  1191.       root = button.parentNode;
  1192.       id = root.id.slice(2);
  1193.       quotes = $$(".quotelink[href$='#p" + id + "'], .backlink[href$='#p" + id + "']");
  1194.       if (/\bstub\b/.test(button.className)) {
  1195.         ReplyHiding.show(root);
  1196.         for (_i = 0, _len = quotes.length; _i < _len; _i++) {
  1197.           quote = quotes[_i];
  1198.           $.rmClass(quote, 'filtered');
  1199.         }
  1200.         delete g.hiddenReplies[id];
  1201.       } else {
  1202.         ReplyHiding.hide(root);
  1203.         for (_j = 0, _len1 = quotes.length; _j < _len1; _j++) {
  1204.           quote = quotes[_j];
  1205.           $.addClass(quote, 'filtered');
  1206.         }
  1207.         g.hiddenReplies[id] = Date.now();
  1208.       }
  1209.       return $.set("hiddenReplies/" + g.BOARD + "/", g.hiddenReplies);
  1210.     },
  1211.     hide: function(root, show_stub) {
  1212.       var a, el, menuButton, side, stub;
  1213.       if (show_stub == null) {
  1214.         show_stub = Conf['Show Stubs'];
  1215.       }
  1216.       side = $('.sideArrows', root);
  1217.       if (side.hidden) {
  1218.         return;
  1219.       }
  1220.       side.hidden = true;
  1221.       el = side.nextElementSibling;
  1222.       el.hidden = true;
  1223.       if (!show_stub) {
  1224.         return;
  1225.       }
  1226.       stub = $.el('div', {
  1227.         className: 'hide_reply_button stub',
  1228.         innerHTML: '<a href="javascript:;"><span>[ + ]</span> </a>'
  1229.       });
  1230.       a = stub.firstChild;
  1231.       $.on(a, 'click', ReplyHiding.toggle);
  1232.       $.add(a, $.tn(Conf['Anonymize'] ? 'Anonymous' : $('.desktop > .nameBlock', el).textContent));
  1233.       if (Conf['Menu']) {
  1234.         menuButton = Menu.a.cloneNode(true);
  1235.         $.on(menuButton, 'click', Menu.toggle);
  1236.         $.add(stub, [$.tn(' '), menuButton]);
  1237.       }
  1238.       return $.prepend(root, stub);
  1239.     },
  1240.     show: function(root) {
  1241.       var stub;
  1242.       if (stub = $('.stub', root)) {
  1243.         $.rm(stub);
  1244.       }
  1245.       $('.sideArrows', root).hidden = false;
  1246.       return $('.post', root).hidden = false;
  1247.     }
  1248.   };
  1249.  
  1250.   Menu = {
  1251.     entries: [],
  1252.     init: function() {
  1253.       this.a = $.el('a', {
  1254.         className: 'menu_button',
  1255.         href: 'javascript:;',
  1256.         innerHTML: '[<span></span>]'
  1257.       });
  1258.       this.el = $.el('div', {
  1259.         className: 'reply dialog',
  1260.         id: 'menu',
  1261.         tabIndex: 0
  1262.       });
  1263.       $.on(this.el, 'click', function(e) {
  1264.         return e.stopPropagation();
  1265.       });
  1266.       $.on(this.el, 'keydown', this.keybinds);
  1267.       $.on(d, 'AddMenuEntry', function(e) {
  1268.         return Menu.addEntry(e.detail);
  1269.       });
  1270.       return Main.callbacks.push(this.node);
  1271.     },
  1272.     node: function(post) {
  1273.       var a;
  1274.       if (post.isInlined && !post.isCrosspost) {
  1275.         a = $('.menu_button', post.el);
  1276.       } else {
  1277.         a = Menu.a.cloneNode(true);
  1278.         $.add($('.postInfo', post.el), [$.tn('\u00A0'), a]);
  1279.       }
  1280.       return $.on(a, 'click', Menu.toggle);
  1281.     },
  1282.     toggle: function(e) {
  1283.       var lastOpener, post;
  1284.       e.preventDefault();
  1285.       e.stopPropagation();
  1286.       if (Menu.el.parentNode) {
  1287.         lastOpener = Menu.lastOpener;
  1288.         Menu.close();
  1289.         if (lastOpener === this) {
  1290.           return;
  1291.         }
  1292.       }
  1293.       Menu.lastOpener = this;
  1294.       post = /\bhidden_thread\b/.test(this.parentNode.className) ? $.x('ancestor::div[parent::div[@class="board"]]/child::div[contains(@class,"opContainer")]', this) : $.x('ancestor::div[contains(@class,"postContainer")][1]', this);
  1295.       return Menu.open(this, Main.preParse(post));
  1296.     },
  1297.     open: function(button, post) {
  1298.       var bLeft, bRect, bTop, el, entry, funk, mRect, _i, _len, _ref;
  1299.       el = Menu.el;
  1300.       el.setAttribute('data-id', post.ID);
  1301.       el.setAttribute('data-rootid', post.root.id);
  1302.       funk = function(entry, parent) {
  1303.         var child, children, subMenu, _i, _len;
  1304.         children = entry.children;
  1305.         if (!entry.open(post)) {
  1306.           return;
  1307.         }
  1308.         $.add(parent, entry.el);
  1309.         if (!children) {
  1310.           return;
  1311.         }
  1312.         if (subMenu = $('.subMenu', entry.el)) {
  1313.           $.rm(subMenu);
  1314.         }
  1315.         subMenu = $.el('div', {
  1316.           className: 'reply dialog subMenu'
  1317.         });
  1318.         $.add(entry.el, subMenu);
  1319.         for (_i = 0, _len = children.length; _i < _len; _i++) {
  1320.           child = children[_i];
  1321.           funk(child, subMenu);
  1322.         }
  1323.       };
  1324.       _ref = Menu.entries;
  1325.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1326.         entry = _ref[_i];
  1327.         funk(entry, el);
  1328.       }
  1329.       Menu.focus($('.entry', Menu.el));
  1330.       $.on(d, 'click', Menu.close);
  1331.       $.add(d.body, el);
  1332.       mRect = el.getBoundingClientRect();
  1333.       bRect = button.getBoundingClientRect();
  1334.       bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top;
  1335.       bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left;
  1336.       el.style.top = bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight ? bTop + bRect.height + 2 + 'px' : bTop - mRect.height - 2 + 'px';
  1337.       el.style.left = bRect.left + mRect.width < d.documentElement.clientWidth ? bLeft + 'px' : bLeft + bRect.width - mRect.width + 'px';
  1338.       return el.focus();
  1339.     },
  1340.     close: function() {
  1341.       var el, focused, _i, _len, _ref;
  1342.       el = Menu.el;
  1343.       $.rm(el);
  1344.       _ref = $$('.focused.entry', el);
  1345.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1346.         focused = _ref[_i];
  1347.         $.rmClass(focused, 'focused');
  1348.       }
  1349.       el.innerHTML = null;
  1350.       el.removeAttribute('style');
  1351.       delete Menu.lastOpener;
  1352.       delete Menu.focusedEntry;
  1353.       return $.off(d, 'click', Menu.close);
  1354.     },
  1355.     keybinds: function(e) {
  1356.       var el, next, subMenu;
  1357.       el = Menu.focusedEntry;
  1358.       switch (Keybinds.keyCode(e) || e.keyCode) {
  1359.         case 'Esc':
  1360.           Menu.lastOpener.focus();
  1361.           Menu.close();
  1362.           break;
  1363.         case 13:
  1364.         case 32:
  1365.           el.click();
  1366.           break;
  1367.         case 'Up':
  1368.           if (next = el.previousElementSibling) {
  1369.             Menu.focus(next);
  1370.           }
  1371.           break;
  1372.         case 'Down':
  1373.           if (next = el.nextElementSibling) {
  1374.             Menu.focus(next);
  1375.           }
  1376.           break;
  1377.         case 'Right':
  1378.           if ((subMenu = $('.subMenu', el)) && (next = subMenu.firstElementChild)) {
  1379.             Menu.focus(next);
  1380.           }
  1381.           break;
  1382.         case 'Left':
  1383.           if (next = $.x('parent::*[contains(@class,"subMenu")]/parent::*', el)) {
  1384.             Menu.focus(next);
  1385.           }
  1386.           break;
  1387.         default:
  1388.           return;
  1389.       }
  1390.       e.preventDefault();
  1391.       return e.stopPropagation();
  1392.     },
  1393.     focus: function(el) {
  1394.       var focused, _i, _len, _ref;
  1395.       if (focused = $.x('parent::*/child::*[contains(@class,"focused")]', el)) {
  1396.         $.rmClass(focused, 'focused');
  1397.       }
  1398.       _ref = $$('.focused', el);
  1399.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1400.         focused = _ref[_i];
  1401.         $.rmClass(focused, 'focused');
  1402.       }
  1403.       Menu.focusedEntry = el;
  1404.       return $.addClass(el, 'focused');
  1405.     },
  1406.     addEntry: function(entry) {
  1407.       var funk;
  1408.       funk = function(entry) {
  1409.         var child, children, el, _i, _len;
  1410.         el = entry.el, children = entry.children;
  1411.         $.addClass(el, 'entry');
  1412.         $.on(el, 'focus mouseover', function(e) {
  1413.           e.stopPropagation();
  1414.           return Menu.focus(this);
  1415.         });
  1416.         if (!children) {
  1417.           return;
  1418.         }
  1419.         $.addClass(el, 'hasSubMenu');
  1420.         for (_i = 0, _len = children.length; _i < _len; _i++) {
  1421.           child = children[_i];
  1422.           funk(child);
  1423.         }
  1424.       };
  1425.       funk(entry);
  1426.       return Menu.entries.push(entry);
  1427.     }
  1428.   };
  1429.  
  1430.   Keybinds = {
  1431.     init: function() {
  1432.       var node, _i, _len, _ref;
  1433.       _ref = $$('[accesskey]');
  1434.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1435.         node = _ref[_i];
  1436.         node.removeAttribute('accesskey');
  1437.       }
  1438.       return $.on(d, 'keydown', Keybinds.keydown);
  1439.     },
  1440.     keydown: function(e) {
  1441.       var form, key, o, target, thread;
  1442.       if (!(key = Keybinds.keyCode(e))) {
  1443.         return;
  1444.       }
  1445.       target = e.target;
  1446.       if (/TEXTAREA|INPUT/.test(target.nodeName)) {
  1447.         if (!((key === 'Esc') || (/\+/.test(key)))) {
  1448.           return;
  1449.         }
  1450.       }
  1451.       thread = Nav.getThread();
  1452.       switch (key) {
  1453.         case Conf.openQR:
  1454.           Keybinds.qr(thread, true);
  1455.           break;
  1456.         case Conf.openEmptyQR:
  1457.           Keybinds.qr(thread);
  1458.           break;
  1459.         case Conf.openOptions:
  1460.           if (!$.id('overlay')) {
  1461.             Options.dialog();
  1462.           }
  1463.           break;
  1464.         case Conf.close:
  1465.           if (o = $.id('overlay')) {
  1466.             Options.close.call(o);
  1467.           } else if (QR.el) {
  1468.             QR.close();
  1469.           }
  1470.           break;
  1471.         case Conf.submit:
  1472.           if (QR.el && !QR.status()) {
  1473.             QR.submit();
  1474.           }
  1475.           break;
  1476.         case Conf.spoiler:
  1477.           if (target.nodeName !== 'TEXTAREA') {
  1478.             return;
  1479.           }
  1480.           Keybinds.tags('spoiler', target);
  1481.           break;
  1482.         case Conf.code:
  1483.           if (target.nodeName !== 'TEXTAREA') {
  1484.             return;
  1485.           }
  1486.           Keybinds.tags('code', target);
  1487.           break;
  1488.         case Conf.watch:
  1489.           Watcher.toggle(thread);
  1490.           break;
  1491.         case Conf.update:
  1492.           Updater.update();
  1493.           break;
  1494.         case Conf.unreadCountTo0:
  1495.           Unread.replies = [];
  1496.           Unread.update(true);
  1497.           break;
  1498.         case Conf.expandImage:
  1499.           Keybinds.img(thread);
  1500.           break;
  1501.         case Conf.expandAllImages:
  1502.           Keybinds.img(thread, true);
  1503.           break;
  1504.         case Conf.zero:
  1505.           window.location = "/" + g.BOARD + "/0#delform";
  1506.           break;
  1507.         case Conf.nextPage:
  1508.           if (form = $('.next form')) {
  1509.             window.location = form.action;
  1510.           }
  1511.           break;
  1512.         case Conf.previousPage:
  1513.           if (form = $('.prev form')) {
  1514.             window.location = form.action;
  1515.           }
  1516.           break;
  1517.         case Conf.nextThread:
  1518.           if (g.REPLY) {
  1519.             return;
  1520.           }
  1521.           Nav.scroll(+1);
  1522.           break;
  1523.         case Conf.previousThread:
  1524.           if (g.REPLY) {
  1525.             return;
  1526.           }
  1527.           Nav.scroll(-1);
  1528.           break;
  1529.         case Conf.expandThread:
  1530.           ExpandThread.toggle(thread);
  1531.           break;
  1532.         case Conf.openThread:
  1533.           Keybinds.open(thread);
  1534.           break;
  1535.         case Conf.openThreadTab:
  1536.           Keybinds.open(thread, true);
  1537.           break;
  1538.         case Conf.nextReply:
  1539.           Keybinds.hl(+1, thread);
  1540.           break;
  1541.         case Conf.previousReply:
  1542.           Keybinds.hl(-1, thread);
  1543.           break;
  1544.         case Conf.hide:
  1545.           if (/\bthread\b/.test(thread.className)) {
  1546.             ThreadHiding.toggle(thread);
  1547.           }
  1548.           break;
  1549.         default:
  1550.           return;
  1551.       }
  1552.       return e.preventDefault();
  1553.     },
  1554.     keyCode: function(e) {
  1555.       var c, kc, key;
  1556.       key = (function() {
  1557.         switch (kc = e.keyCode) {
  1558.           case 8:
  1559.             return '';
  1560.           case 13:
  1561.             return 'Enter';
  1562.           case 27:
  1563.             return 'Esc';
  1564.           case 37:
  1565.             return 'Left';
  1566.           case 38:
  1567.             return 'Up';
  1568.           case 39:
  1569.             return 'Right';
  1570.           case 40:
  1571.             return 'Down';
  1572.           case 48:
  1573.           case 49:
  1574.           case 50:
  1575.           case 51:
  1576.           case 52:
  1577.           case 53:
  1578.           case 54:
  1579.           case 55:
  1580.           case 56:
  1581.           case 57:
  1582.           case 65:
  1583.           case 66:
  1584.           case 67:
  1585.           case 68:
  1586.           case 69:
  1587.           case 70:
  1588.           case 71:
  1589.           case 72:
  1590.           case 73:
  1591.           case 74:
  1592.           case 75:
  1593.           case 76:
  1594.           case 77:
  1595.           case 78:
  1596.           case 79:
  1597.           case 80:
  1598.           case 81:
  1599.           case 82:
  1600.           case 83:
  1601.           case 84:
  1602.           case 85:
  1603.           case 86:
  1604.           case 87:
  1605.           case 88:
  1606.           case 89:
  1607.           case 90:
  1608.             c = String.fromCharCode(kc);
  1609.             if (e.shiftKey) {
  1610.               return c;
  1611.             } else {
  1612.               return c.toLowerCase();
  1613.             }
  1614.             break;
  1615.           default:
  1616.             return null;
  1617.         }
  1618.       })();
  1619.       if (key) {
  1620.         if (e.altKey) {
  1621.           key = 'alt+' + key;
  1622.         }
  1623.         if (e.ctrlKey) {
  1624.           key = 'ctrl+' + key;
  1625.         }
  1626.         if (e.metaKey) {
  1627.           key = 'meta+' + key;
  1628.         }
  1629.       }
  1630.       return key;
  1631.     },
  1632.     tags: function(tag, ta) {
  1633.       var range, selEnd, selStart, value;
  1634.       value = ta.value;
  1635.       selStart = ta.selectionStart;
  1636.       selEnd = ta.selectionEnd;
  1637.       ta.value = value.slice(0, selStart) + ("[" + tag + "]") + value.slice(selStart, selEnd) + ("[/" + tag + "]") + value.slice(selEnd);
  1638.       range = ("[" + tag + "]").length + selEnd;
  1639.       ta.setSelectionRange(range, range);
  1640.       return $.event(ta, new Event('input'));
  1641.     },
  1642.     img: function(thread, all) {
  1643.       var thumb;
  1644.       if (all) {
  1645.         return $.id('imageExpand').click();
  1646.       } else {
  1647.         thumb = $('img[data-md5]', $('.post.highlight', thread) || thread);
  1648.         return ImageExpand.toggle(thumb.parentNode);
  1649.       }
  1650.     },
  1651.     qr: function(thread, quote) {
  1652.       if (quote) {
  1653.         QR.quote.call($('a[title="Quote this post"]', $('.post.highlight', thread) || thread));
  1654.       } else {
  1655.         QR.open();
  1656.       }
  1657.       return $('textarea', QR.el).focus();
  1658.     },
  1659.     open: function(thread, tab) {
  1660.       var id, url;
  1661.       if (g.REPLY) {
  1662.         return;
  1663.       }
  1664.       id = thread.id.slice(1);
  1665.       url = "//boards.4chan.org/" + g.BOARD + "/thread/" + id;
  1666.       if (tab) {
  1667.         return $.open(url);
  1668.       } else {
  1669.         return location.href = url;
  1670.       }
  1671.     },
  1672.     hl: function(delta, thread) {
  1673.       var next, post, rect, replies, reply, _i, _len;
  1674.       if (post = $('.reply.highlight', thread)) {
  1675.         $.rmClass(post, 'highlight');
  1676.         post.removeAttribute('tabindex');
  1677.         rect = post.getBoundingClientRect();
  1678.         if (rect.bottom >= 0 && rect.top <= d.documentElement.clientHeight) {
  1679.           next = $.x('child::div[contains(@class,"post reply")]', delta === +1 ? post.parentNode.nextElementSibling : post.parentNode.previousElementSibling);
  1680.           if (!next) {
  1681.             this.focus(post);
  1682.             return;
  1683.           }
  1684.           if (!(g.REPLY || $.x('ancestor::div[parent::div[@class="board"]]', next) === thread)) {
  1685.             return;
  1686.           }
  1687.           rect = next.getBoundingClientRect();
  1688.           if (rect.top < 0 || rect.bottom > d.documentElement.clientHeight) {
  1689.             next.scrollIntoView(delta === -1);
  1690.           }
  1691.           this.focus(next);
  1692.           return;
  1693.         }
  1694.       }
  1695.       replies = $$('.reply', thread);
  1696.       if (delta === -1) {
  1697.         replies.reverse();
  1698.       }
  1699.       for (_i = 0, _len = replies.length; _i < _len; _i++) {
  1700.         reply = replies[_i];
  1701.         rect = reply.getBoundingClientRect();
  1702.         if (delta === +1 && rect.top >= 0 || delta === -1 && rect.bottom <= d.documentElement.clientHeight) {
  1703.           this.focus(reply);
  1704.           return;
  1705.         }
  1706.       }
  1707.     },
  1708.     focus: function(post) {
  1709.       $.addClass(post, 'highlight');
  1710.       post.tabIndex = 0;
  1711.       return post.focus();
  1712.     }
  1713.   };
  1714.  
  1715.   Nav = {
  1716.     init: function() {
  1717.       var next, prev, span;
  1718.       span = $.el('span', {
  1719.         id: 'navlinks'
  1720.       });
  1721.       prev = $.el('a', {
  1722.         textContent: '▲',
  1723.         href: 'javascript:;'
  1724.       });
  1725.       next = $.el('a', {
  1726.         textContent: '▼',
  1727.         href: 'javascript:;'
  1728.       });
  1729.       $.on(prev, 'click', this.prev);
  1730.       $.on(next, 'click', this.next);
  1731.       $.add(span, [prev, $.tn(' '), next]);
  1732.       return $.add(d.body, span);
  1733.     },
  1734.     prev: function() {
  1735.       if (g.REPLY) {
  1736.         return window.scrollTo(0, 0);
  1737.       } else {
  1738.         return Nav.scroll(-1);
  1739.       }
  1740.     },
  1741.     next: function() {
  1742.       if (g.REPLY) {
  1743.         return window.scrollTo(0, d.body.scrollHeight);
  1744.       } else {
  1745.         return Nav.scroll(+1);
  1746.       }
  1747.     },
  1748.     getThread: function(full) {
  1749.       var bottom, i, rect, thread, _i, _len, _ref;
  1750.       Nav.threads = $$('.thread:not([hidden])');
  1751.       _ref = Nav.threads;
  1752.       for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
  1753.         thread = _ref[i];
  1754.         rect = thread.getBoundingClientRect();
  1755.         bottom = rect.bottom;
  1756.         if (bottom > 0) {
  1757.           if (full) {
  1758.             return [thread, i, rect];
  1759.           }
  1760.           return thread;
  1761.         }
  1762.       }
  1763.       return $('.board');
  1764.     },
  1765.     scroll: function(delta) {
  1766.       var i, rect, thread, top, _ref, _ref1;
  1767.       _ref = Nav.getThread(true), thread = _ref[0], i = _ref[1], rect = _ref[2];
  1768.       top = rect.top;
  1769.       if (!((delta === -1 && Math.ceil(top) < 0) || (delta === +1 && top > 1))) {
  1770.         i += delta;
  1771.       }
  1772.       top = (_ref1 = Nav.threads[i]) != null ? _ref1.getBoundingClientRect().top : void 0;
  1773.       return window.scrollBy(0, top);
  1774.     }
  1775.   };
  1776.  
  1777.   QR = {
  1778.     init: function() {
  1779.       if (!$.id('postForm')) {
  1780.         return;
  1781.       }
  1782.       Main.callbacks.push(this.node);
  1783.       return setTimeout(this.asyncInit);
  1784.     },
  1785.     asyncInit: function() {
  1786.       var link;
  1787.       if (Conf['Hide Original Post Form']) {
  1788.         link = $.el('h1', {
  1789.           innerHTML: "<a href=javascript:;>" + (g.REPLY ? 'Reply to Thread' : 'Start a Thread') + "</a>"
  1790.         });
  1791.         $.on(link.firstChild, 'click', function() {
  1792.           QR.open();
  1793.           if (!g.REPLY) {
  1794.             QR.threadSelector.value = g.BOARD === 'f' ? '9999' : 'new';
  1795.           }
  1796.           return $('textarea', QR.el).focus();
  1797.         });
  1798.         $.before($.id('postForm'), link);
  1799.       }
  1800.       if (Conf['Persistent QR']) {
  1801.         QR.dialog();
  1802.         if (Conf['Auto Hide QR']) {
  1803.           QR.hide();
  1804.         }
  1805.       }
  1806.       $.on(d, 'dragover', QR.dragOver);
  1807.       $.on(d, 'drop', QR.dropFile);
  1808.       return $.on(d, 'dragstart dragend', QR.drag);
  1809.     },
  1810.     node: function(post) {
  1811.       return $.on($('a[title="Quote this post"]', $('.postInfo', post.el)), 'click', QR.quote);
  1812.     },
  1813.     open: function() {
  1814.       if (QR.el) {
  1815.         QR.el.hidden = false;
  1816.         return QR.unhide();
  1817.       } else {
  1818.         return QR.dialog();
  1819.       }
  1820.     },
  1821.     close: function() {
  1822.       var i, spoiler, _i, _len, _ref;
  1823.       QR.el.hidden = true;
  1824.       QR.abort();
  1825.       d.activeElement.blur();
  1826.       $.rmClass(QR.el, 'dump');
  1827.       _ref = QR.replies;
  1828.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  1829.         i = _ref[_i];
  1830.         QR.replies[0].rm();
  1831.       }
  1832.       QR.cooldown.auto = false;
  1833.       QR.status();
  1834.       QR.resetFileInput();
  1835.       if (!Conf['Remember Spoiler'] && (spoiler = $.id('spoiler')).checked) {
  1836.         spoiler.click();
  1837.       }
  1838.       return QR.cleanError();
  1839.     },
  1840.     hide: function() {
  1841.       d.activeElement.blur();
  1842.       $.addClass(QR.el, 'autohide');
  1843.       return $.id('autohide').checked = true;
  1844.     },
  1845.     unhide: function() {
  1846.       $.rmClass(QR.el, 'autohide');
  1847.       return $.id('autohide').checked = false;
  1848.     },
  1849.     toggleHide: function() {
  1850.       return this.checked && QR.hide() || QR.unhide();
  1851.     },
  1852.     error: function(err) {
  1853.       var el;
  1854.       el = $('.warning', QR.el);
  1855.       if (typeof err === 'string') {
  1856.         el.textContent = err;
  1857.       } else {
  1858.         el.innerHTML = null;
  1859.         $.add(el, err);
  1860.       }
  1861.       QR.open();
  1862.       if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) {
  1863.         $('[autocomplete]', QR.el).focus();
  1864.       }
  1865.       if (d.hidden) {
  1866.         return alert(el.textContent);
  1867.       }
  1868.     },
  1869.     cleanError: function() {
  1870.       return $('.warning', QR.el).textContent = null;
  1871.     },
  1872.     status: function(data) {
  1873.       var disabled, input, value;
  1874.       if (data == null) {
  1875.         data = {};
  1876.       }
  1877.       if (!QR.el) {
  1878.         return;
  1879.       }
  1880.       if (g.dead) {
  1881.         value = 404;
  1882.         disabled = true;
  1883.         QR.cooldown.auto = false;
  1884.       }
  1885.       value = data.progress || QR.cooldown.seconds || value;
  1886.       input = QR.status.input;
  1887.       input.value = QR.cooldown.auto && Conf['Cooldown'] ? value ? "Auto " + value : 'Auto' : value || 'Submit';
  1888.       return input.disabled = disabled || false;
  1889.     },
  1890.     cooldown: {
  1891.       init: function() {
  1892.         if (!Conf['Cooldown']) {
  1893.           return;
  1894.         }
  1895.         QR.cooldown.types = {
  1896.           thread: (function() {
  1897.             switch (g.BOARD) {
  1898.               case 'b':
  1899.               case 'soc':
  1900.               case 'r9k':
  1901.                 return 600;
  1902.               default:
  1903.                 return 300;
  1904.             }
  1905.           })(),
  1906.           sage: 60,
  1907.           file: 60,
  1908.           post: 60
  1909.         };
  1910.         QR.cooldown.cooldowns = $.get("" + g.BOARD + ".cooldown", {});
  1911.         QR.cooldown.start();
  1912.         return $.sync("" + g.BOARD + ".cooldown", QR.cooldown.sync);
  1913.       },
  1914.       start: function() {
  1915.         if (QR.cooldown.isCounting) {
  1916.           return;
  1917.         }
  1918.         QR.cooldown.isCounting = true;
  1919.         return QR.cooldown.count();
  1920.       },
  1921.       sync: function(cooldowns) {
  1922.         var id;
  1923.         for (id in cooldowns) {
  1924.           QR.cooldown.cooldowns[id] = cooldowns[id];
  1925.         }
  1926.         return QR.cooldown.start();
  1927.       },
  1928.       set: function(data) {
  1929.         var cooldown, hasFile, isReply, isSage, start, type;
  1930.         if (!Conf['Cooldown']) {
  1931.           return;
  1932.         }
  1933.         start = Date.now();
  1934.         if (data.delay) {
  1935.           cooldown = {
  1936.             delay: data.delay
  1937.           };
  1938.         } else {
  1939.           isSage = /sage/i.test(data.post.email);
  1940.           hasFile = !!data.post.file;
  1941.           isReply = data.isReply;
  1942.           type = !isReply ? 'thread' : isSage ? 'sage' : hasFile ? 'file' : 'post';
  1943.           cooldown = {
  1944.             isReply: isReply,
  1945.             isSage: isSage,
  1946.             hasFile: hasFile,
  1947.             timeout: start + QR.cooldown.types[type] * $.SECOND
  1948.           };
  1949.         }
  1950.         QR.cooldown.cooldowns[start] = cooldown;
  1951.         $.set("" + g.BOARD + ".cooldown", QR.cooldown.cooldowns);
  1952.         return QR.cooldown.start();
  1953.       },
  1954.       unset: function(id) {
  1955.         delete QR.cooldown.cooldowns[id];
  1956.         return $.set("" + g.BOARD + ".cooldown", QR.cooldown.cooldowns);
  1957.       },
  1958.       count: function() {
  1959.         var cooldown, cooldowns, elapsed, hasFile, isReply, isSage, now, post, seconds, start, type, types, update, _ref;
  1960.         if (Object.keys(QR.cooldown.cooldowns).length) {
  1961.           setTimeout(QR.cooldown.count, 1000);
  1962.         } else {
  1963.           $["delete"]("" + g.BOARD + ".cooldown");
  1964.           delete QR.cooldown.isCounting;
  1965.           delete QR.cooldown.seconds;
  1966.           QR.status();
  1967.           return;
  1968.         }
  1969.         if ((isReply = g.REPLY ? true : QR.threadSelector.value !== 'new')) {
  1970.           post = QR.replies[0];
  1971.           isSage = /sage/i.test(post.email);
  1972.           hasFile = !!post.file;
  1973.         }
  1974.         now = Date.now();
  1975.         seconds = null;
  1976.         _ref = QR.cooldown, types = _ref.types, cooldowns = _ref.cooldowns;
  1977.         for (start in cooldowns) {
  1978.           cooldown = cooldowns[start];
  1979.           if ('delay' in cooldown) {
  1980.             if (cooldown.delay) {
  1981.               seconds = Math.max(seconds, cooldown.delay--);
  1982.             } else {
  1983.               seconds = Math.max(seconds, 0);
  1984.               QR.cooldown.unset(start);
  1985.             }
  1986.             continue;
  1987.           }
  1988.           if (isReply === cooldown.isReply) {
  1989.             type = !isReply ? 'thread' : isSage && cooldown.isSage ? 'sage' : hasFile && cooldown.hasFile ? 'file' : 'post';
  1990.             elapsed = Math.floor((now - start) / 1000);
  1991.             if (elapsed >= 0) {
  1992.               seconds = Math.max(seconds, types[type] - elapsed);
  1993.             }
  1994.           }
  1995.           if (!((start <= now && now <= cooldown.timeout))) {
  1996.             QR.cooldown.unset(start);
  1997.           }
  1998.         }
  1999.         update = seconds !== null || !!QR.cooldown.seconds;
  2000.         QR.cooldown.seconds = seconds;
  2001.         if (update) {
  2002.           QR.status();
  2003.         }
  2004.         if (seconds === 0 && QR.cooldown.auto) {
  2005.           return QR.submit();
  2006.         }
  2007.       }
  2008.     },
  2009.     quote: function(e) {
  2010.       var caretPos, id, range, s, sel, ta, text, _ref;
  2011.       if (e != null) {
  2012.         e.preventDefault();
  2013.       }
  2014.       QR.open();
  2015.       ta = $('textarea', QR.el);
  2016.       if (!(g.REPLY || ta.value)) {
  2017.         QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', this).id.slice(1);
  2018.       }
  2019.       id = this.previousSibling.hash.slice(2);
  2020.       text = ">>" + id + "\n";
  2021.       sel = d.getSelection();
  2022.       if ((s = sel.toString().trim()) && id === ((_ref = $.x('ancestor-or-self::blockquote', sel.anchorNode)) != null ? _ref.id.match(/\d+$/)[0] : void 0)) {
  2023.         s = s.replace(/\n/g, '\n>');
  2024.         text += ">" + s + "\n";
  2025.       }
  2026.       caretPos = ta.selectionStart;
  2027.       ta.value = ta.value.slice(0, caretPos) + text + ta.value.slice(ta.selectionEnd);
  2028.       range = caretPos + text.length;
  2029.       ta.setSelectionRange(range, range);
  2030.       ta.focus();
  2031.       return $.event(ta, new Event('input'));
  2032.     },
  2033.     characterCount: function() {
  2034.       var count, counter;
  2035.       counter = QR.charaCounter;
  2036.       count = this.textLength;
  2037.       counter.textContent = count;
  2038.       counter.hidden = count < 1000;
  2039.       return (count > 1500 ? $.addClass : $.rmClass)(counter, 'warning');
  2040.     },
  2041.     drag: function(e) {
  2042.       var toggle;
  2043.       toggle = e.type === 'dragstart' ? $.off : $.on;
  2044.       toggle(d, 'dragover', QR.dragOver);
  2045.       return toggle(d, 'drop', QR.dropFile);
  2046.     },
  2047.     dragOver: function(e) {
  2048.       e.preventDefault();
  2049.       return e.dataTransfer.dropEffect = 'copy';
  2050.     },
  2051.     dropFile: function(e) {
  2052.       if (!e.dataTransfer.files.length) {
  2053.         return;
  2054.       }
  2055.       e.preventDefault();
  2056.       QR.open();
  2057.       QR.fileInput.call(e.dataTransfer);
  2058.       return $.addClass(QR.el, 'dump');
  2059.     },
  2060.     fileInput: function() {
  2061.       var file, _i, _len, _ref;
  2062.       QR.cleanError();
  2063.       if (this.files.length === 1) {
  2064.         file = this.files[0];
  2065.         if (file.size > this.max) {
  2066.           QR.error('File too large.');
  2067.           QR.resetFileInput();
  2068.         } else if (-1 === QR.mimeTypes.indexOf(file.type)) {
  2069.           QR.error('Unsupported file type.');
  2070.           QR.resetFileInput();
  2071.         } else {
  2072.           QR.selected.setFile(file);
  2073.         }
  2074.         return;
  2075.       }
  2076.       _ref = this.files;
  2077.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  2078.         file = _ref[_i];
  2079.         if (file.size > this.max) {
  2080.           QR.error("File " + file.name + " is too large.");
  2081.           break;
  2082.         } else if (-1 === QR.mimeTypes.indexOf(file.type)) {
  2083.           QR.error("" + file.name + ": Unsupported file type.");
  2084.           break;
  2085.         }
  2086.         if (!QR.replies[QR.replies.length - 1].file) {
  2087.           QR.replies[QR.replies.length - 1].setFile(file);
  2088.         } else {
  2089.           new QR.reply().setFile(file);
  2090.         }
  2091.       }
  2092.       $.addClass(QR.el, 'dump');
  2093.       return QR.resetFileInput();
  2094.     },
  2095.     resetFileInput: function() {
  2096.       return $('[type=file]', QR.el).value = null;
  2097.     },
  2098.     replies: [],
  2099.     reply: (function() {
  2100.       function _Class() {
  2101.         var persona, prev,
  2102.           _this = this;
  2103.         prev = QR.replies[QR.replies.length - 1];
  2104.         persona = $.get('QR.persona', {});
  2105.         this.name = prev ? prev.name : persona.name || null;
  2106.         this.email = prev && !/^sage$/.test(prev.email) ? prev.email : persona.email || null;
  2107.         this.sub = prev && Conf['Remember Subject'] ? prev.sub : Conf['Remember Subject'] ? persona.sub : null;
  2108.         this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false;
  2109.         this.com = null;
  2110.         this.el = $.el('a', {
  2111.           className: 'thumbnail',
  2112.           draggable: true,
  2113.           href: 'javascript:;',
  2114.           innerHTML: '<a class=remove>×</a><label hidden><input type=checkbox> Spoiler</label><span></span>'
  2115.         });
  2116.         $('input', this.el).checked = this.spoiler;
  2117.         $.on(this.el, 'click', function() {
  2118.           return _this.select();
  2119.         });
  2120.         $.on($('.remove', this.el), 'click', function(e) {
  2121.           e.stopPropagation();
  2122.           return _this.rm();
  2123.         });
  2124.         $.on($('label', this.el), 'click', function(e) {
  2125.           return e.stopPropagation();
  2126.         });
  2127.         $.on($('input', this.el), 'change', function(e) {
  2128.           _this.spoiler = e.target.checked;
  2129.           if (_this.el.id === 'selected') {
  2130.             return $.id('spoiler').checked = _this.spoiler;
  2131.           }
  2132.         });
  2133.         $.before($('#addReply', QR.el), this.el);
  2134.         $.on(this.el, 'dragstart', this.dragStart);
  2135.         $.on(this.el, 'dragenter', this.dragEnter);
  2136.         $.on(this.el, 'dragleave', this.dragLeave);
  2137.         $.on(this.el, 'dragover', this.dragOver);
  2138.         $.on(this.el, 'dragend', this.dragEnd);
  2139.         $.on(this.el, 'drop', this.drop);
  2140.         QR.replies.push(this);
  2141.       }
  2142.  
  2143.       _Class.prototype.setFile = function(file) {
  2144.         var fileUrl, img, url,
  2145.           _this = this;
  2146.         this.file = file;
  2147.         this.el.title = "" + file.name + " (" + ($.bytesToString(file.size)) + ")";
  2148.         if (QR.spoiler) {
  2149.           $('label', this.el).hidden = false;
  2150.         }
  2151.         if (!/^image/.test(file.type)) {
  2152.           this.el.style.backgroundImage = null;
  2153.           return;
  2154.         }
  2155.         if (!(url = window.URL || window.webkitURL)) {
  2156.           return;
  2157.         }
  2158.         url.revokeObjectURL(this.url);
  2159.         fileUrl = url.createObjectURL(file);
  2160.         img = $.el('img');
  2161.         $.on(img, 'load', function() {
  2162.           var c, data, i, l, s, ui8a, _i;
  2163.           s = 90 * 3;
  2164.           if (img.height < s || img.width < s) {
  2165.             _this.url = fileUrl;
  2166.             _this.el.style.backgroundImage = "url(" + _this.url + ")";
  2167.             return;
  2168.           }
  2169.           if (img.height <= img.width) {
  2170.             img.width = s / img.height * img.width;
  2171.             img.height = s;
  2172.           } else {
  2173.             img.height = s / img.width * img.height;
  2174.             img.width = s;
  2175.           }
  2176.           c = $.el('canvas');
  2177.           c.height = img.height;
  2178.           c.width = img.width;
  2179.           c.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
  2180.           data = atob(c.toDataURL().split(',')[1]);
  2181.           l = data.length;
  2182.           ui8a = new Uint8Array(l);
  2183.           for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) {
  2184.             ui8a[i] = data.charCodeAt(i);
  2185.           }
  2186.           _this.url = url.createObjectURL(new Blob([ui8a], {
  2187.             type: 'image/png'
  2188.           }));
  2189.           _this.el.style.backgroundImage = "url(" + _this.url + ")";
  2190.           return typeof url.revokeObjectURL === "function" ? url.revokeObjectURL(fileUrl) : void 0;
  2191.         });
  2192.         return img.src = fileUrl;
  2193.       };
  2194.  
  2195.       _Class.prototype.rmFile = function() {
  2196.         var _base1;
  2197.         QR.resetFileInput();
  2198.         delete this.file;
  2199.         this.el.title = null;
  2200.         this.el.style.backgroundImage = null;
  2201.         if (QR.spoiler) {
  2202.           $('label', this.el).hidden = true;
  2203.         }
  2204.         return typeof (_base1 = window.URL || window.webkitURL).revokeObjectURL === "function" ? _base1.revokeObjectURL(this.url) : void 0;
  2205.       };
  2206.  
  2207.       _Class.prototype.select = function() {
  2208.         var data, rectEl, rectList, _i, _len, _ref, _ref1;
  2209.         if ((_ref = QR.selected) != null) {
  2210.           _ref.el.id = null;
  2211.         }
  2212.         QR.selected = this;
  2213.         this.el.id = 'selected';
  2214.         rectEl = this.el.getBoundingClientRect();
  2215.         rectList = this.el.parentNode.getBoundingClientRect();
  2216.         this.el.parentNode.scrollLeft += rectEl.left + rectEl.width / 2 - rectList.left - rectList.width / 2;
  2217.         _ref1 = ['name', 'email', 'sub', 'com'];
  2218.         for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
  2219.           data = _ref1[_i];
  2220.           $("[name=" + data + "]", QR.el).value = this[data];
  2221.         }
  2222.         QR.characterCount.call($('textarea', QR.el));
  2223.         return $('#spoiler', QR.el).checked = this.spoiler;
  2224.       };
  2225.  
  2226.       _Class.prototype.dragStart = function() {
  2227.         return $.addClass(this, 'drag');
  2228.       };
  2229.  
  2230.       _Class.prototype.dragEnter = function() {
  2231.         return $.addClass(this, 'over');
  2232.       };
  2233.  
  2234.       _Class.prototype.dragLeave = function() {
  2235.         return $.rmClass(this, 'over');
  2236.       };
  2237.  
  2238.       _Class.prototype.dragOver = function(e) {
  2239.         e.preventDefault();
  2240.         return e.dataTransfer.dropEffect = 'move';
  2241.       };
  2242.  
  2243.       _Class.prototype.drop = function() {
  2244.         var el, index, newIndex, oldIndex, reply;
  2245.         el = $('.drag', this.parentNode);
  2246.         index = function(el) {
  2247.           return Array.prototype.slice.call(el.parentNode.children).indexOf(el);
  2248.         };
  2249.         oldIndex = index(el);
  2250.         newIndex = index(this);
  2251.         if (oldIndex < newIndex) {
  2252.           $.after(this, el);
  2253.         } else {
  2254.           $.before(this, el);
  2255.         }
  2256.         reply = QR.replies.splice(oldIndex, 1)[0];
  2257.         return QR.replies.splice(newIndex, 0, reply);
  2258.       };
  2259.  
  2260.       _Class.prototype.dragEnd = function() {
  2261.         var el;
  2262.         $.rmClass(this, 'drag');
  2263.         if (el = $('.over', this.parentNode)) {
  2264.           return $.rmClass(el, 'over');
  2265.         }
  2266.       };
  2267.  
  2268.       _Class.prototype.rm = function() {
  2269.         var index, _base1;
  2270.         QR.resetFileInput();
  2271.         $.rm(this.el);
  2272.         index = QR.replies.indexOf(this);
  2273.         if (QR.replies.length === 1) {
  2274.           new QR.reply().select();
  2275.         } else if (this.el.id === 'selected') {
  2276.           (QR.replies[index - 1] || QR.replies[index + 1]).select();
  2277.         }
  2278.         QR.replies.splice(index, 1);
  2279.         return typeof (_base1 = window.URL || window.webkitURL).revokeObjectURL === "function" ? _base1.revokeObjectURL(this.url) : void 0;
  2280.       };
  2281.  
  2282.       return _Class;
  2283.  
  2284.     })(),
  2285.     captcha: {
  2286.       init: function() {
  2287.         var _this = this;
  2288.         if (-1 !== d.cookie.indexOf('pass_enabled=')) {
  2289.           return;
  2290.         }
  2291.         if (!(this.isEnabled = !!$.id('captchaFormPart'))) {
  2292.           return;
  2293.         }
  2294.         if ($.id('recaptcha_challenge_field_holder')) {
  2295.           return this.ready();
  2296.         } else {
  2297.           this.onready = function() {
  2298.             return _this.ready();
  2299.           };
  2300.           return $.on($.id('captchaContainer'), 'DOMNodeInserted', this.onready);
  2301.         }
  2302.       },
  2303.       ready: function() {
  2304.         var challenge;
  2305.         if (QR.captcha.challenge = challenge = $.id('recaptcha_challenge_field_holder')) {
  2306.           $.off($.id('captchaContainer'), 'DOMNodeInserted', this.onready);
  2307.           delete this.onready;
  2308.         } else {
  2309.           return;
  2310.         }
  2311.         $.addClass(QR.el, 'captcha');
  2312.         $.after($('.textarea', QR.el), $.el('div', {
  2313.           className: 'captchaimg',
  2314.           title: 'Reload',
  2315.           innerHTML: '<img>'
  2316.         }));
  2317.         $.after($('.captchaimg', QR.el), $.el('div', {
  2318.           className: 'captchainput',
  2319.           innerHTML: '<input title=Verification class=field autocomplete=off size=1>'
  2320.         }));
  2321.         this.img = $('.captchaimg > img', QR.el);
  2322.         this.input = $('.captchainput > input', QR.el);
  2323.         $.on(this.img.parentNode, 'click', this.reload);
  2324.         $.on(this.input, 'keydown', this.keydown);
  2325.         $.on(d.body, 'captcha:reload', this.load.bind(this));
  2326.         return this.reload();
  2327.       },
  2328.       load: function() {
  2329.         var challenge;
  2330.         challenge = this.challenge.firstChild.value;
  2331.         this.img.alt = challenge;
  2332.         this.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge;
  2333.         return this.input.value = null;
  2334.       },
  2335.       reload: function(focus) {
  2336.         $.globalEval('javascript:Recaptcha.reload("t")');
  2337.         if (focus) {
  2338.           return QR.captcha.input.focus();
  2339.         }
  2340.       },
  2341.       keydown: function(e) {
  2342.         var c;
  2343.         c = QR.captcha;
  2344.         if (e.keyCode === 8 && !c.input.value) {
  2345.           c.reload();
  2346.         } else {
  2347.           return;
  2348.         }
  2349.         return e.preventDefault();
  2350.       }
  2351.     },
  2352.     dialog: function() {
  2353.       var fileInput, id, mimeTypes, name, spoiler, ta, thread, threads, _i, _j, _len, _len1, _ref, _ref1;
  2354.       QR.el = UI.dialog('qr', 'top:0;right:0;', '\
  2355. <div class=move>\
  2356.  Quick Reply <input type=checkbox id=autohide title=Auto-hide>\
  2357.  <span> <a class=close title=Close>×</a></span>\
  2358. </div>\
  2359. <form>\
  2360.  <div><input id=dump type=button title="Dump list" value=+ class=field><input name=name title=Name placeholder=Name class=field size=1><input name=email title=E-mail placeholder=E-mail class=field size=1><input name=sub title=Subject placeholder=Subject class=field size=1></div>\
  2361.  <div id=replies><div><a id=addReply href=javascript:; title="Add a reply">+</a></div></div>\
  2362.  <div class=textarea><textarea name=com title=Comment placeholder=Comment class=field></textarea><span id=charCount></span></div>\
  2363.  <div><input type=file title="Shift+Click to remove the selected file." multiple size=16><input type=submit></div>\
  2364.  <label id=spoilerLabel><input type=checkbox id=spoiler> Spoiler Image</label>\
  2365.  <div class=warning></div>\
  2366. </form>');
  2367.       if (Conf['Remember QR size'] && $.engine === 'gecko') {
  2368.         $.on(ta = $('textarea', QR.el), 'mouseup', function() {
  2369.           return $.set('QR.size', this.style.cssText);
  2370.         });
  2371.         ta.style.cssText = $.get('QR.size', '');
  2372.       }
  2373.       QR.mimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/x-shockwave-flash', 'video/webm', ''];
  2374.       fileInput = $('input[type=file]', QR.el);
  2375.       fileInput.max = $('input[name=MAX_FILE_SIZE]').value;
  2376.       if ($.engine !== 'presto') {
  2377.         fileInput.accept = mimeTypes;
  2378.       }
  2379.       QR.spoiler = !!$('input[name=spoiler]');
  2380.       spoiler = $('#spoilerLabel', QR.el);
  2381.       spoiler.hidden = !QR.spoiler;
  2382.       QR.charaCounter = $('#charCount', QR.el);
  2383.       ta = $('textarea', QR.el);
  2384.       if (!g.REPLY) {
  2385.         threads = '<option value=new>New thread</option>';
  2386.         _ref = $$('.thread');
  2387.         for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  2388.           thread = _ref[_i];
  2389.           id = thread.id.slice(1);
  2390.           threads += "<option value=" + id + ">Thread " + id + "</option>";
  2391.         }
  2392.         QR.threadSelector = g.BOARD === 'f' ? $('select[name=filetag]').cloneNode(true) : $.el('select', {
  2393.           innerHTML: threads,
  2394.           title: 'Create a new thread / Reply to a thread'
  2395.         });
  2396.         $.prepend($('.move > span', QR.el), QR.threadSelector);
  2397.         $.on(QR.threadSelector, 'mousedown', function(e) {
  2398.           return e.stopPropagation();
  2399.         });
  2400.       }
  2401.       $.on($('#autohide', QR.el), 'change', QR.toggleHide);
  2402.       $.on($('.close', QR.el), 'click', QR.close);
  2403.       $.on($('#dump', QR.el), 'click', function() {
  2404.         return QR.el.classList.toggle('dump');
  2405.       });
  2406.       $.on($('#addReply', QR.el), 'click', function() {
  2407.         return new QR.reply().select();
  2408.       });
  2409.       $.on($('form', QR.el), 'submit', QR.submit);
  2410.       $.on(ta, 'input', function() {
  2411.         return QR.selected.el.lastChild.textContent = this.value;
  2412.       });
  2413.       $.on(ta, 'input', QR.characterCount);
  2414.       $.on(fileInput, 'change', QR.fileInput);
  2415.       $.on(fileInput, 'click', function(e) {
  2416.         if (e.shiftKey) {
  2417.           return QR.selected.rmFile() || e.preventDefault();
  2418.         }
  2419.       });
  2420.       $.on(spoiler.firstChild, 'change', function() {
  2421.         return $('input', QR.selected.el).click();
  2422.       });
  2423.       $.on($('.warning', QR.el), 'click', QR.cleanError);
  2424.       new QR.reply().select();
  2425.       _ref1 = ['name', 'email', 'sub', 'com'];
  2426.       for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
  2427.         name = _ref1[_j];
  2428.         $.on($("[name=" + name + "]", QR.el), 'input', function() {
  2429.           var _ref2;
  2430.           QR.selected[this.name] = this.value;
  2431.           if (QR.cooldown.auto && QR.selected === QR.replies[0] && (0 < (_ref2 = QR.cooldown.seconds) && _ref2 <= 5)) {
  2432.             return QR.cooldown.auto = false;
  2433.           }
  2434.         });
  2435.       }
  2436.       QR.status.input = $('input[type=submit]', QR.el);
  2437.       QR.status();
  2438.       QR.cooldown.init();
  2439.       QR.captcha.init();
  2440.       $.add(d.body, QR.el);
  2441.       return $.event(QR.el, new CustomEvent('QRDialogCreation', {
  2442.         bubbles: true
  2443.       }));
  2444.     },
  2445.     submit: function(e) {
  2446.       var callbacks, captcha, captchas, challenge, err, filetag, m, opts, post, reply, response, textOnly, threadID;
  2447.       if (e != null) {
  2448.         e.preventDefault();
  2449.       }
  2450.       if (QR.cooldown.seconds) {
  2451.         QR.cooldown.auto = !QR.cooldown.auto;
  2452.         QR.status();
  2453.         return;
  2454.       }
  2455.       QR.abort();
  2456.       reply = QR.replies[0];
  2457.       if (g.BOARD === 'f' && !g.REPLY) {
  2458.         filetag = QR.threadSelector.value;
  2459.         threadID = 'new';
  2460.       } else {
  2461.         threadID = g.THREAD_ID || QR.threadSelector.value;
  2462.       }
  2463.       if (threadID === 'new') {
  2464.         threadID = null;
  2465.         if (g.BOARD === 'vg' && !reply.sub) {
  2466.           err = 'New threads require a subject.';
  2467.         } else if (!(reply.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) {
  2468.           err = 'No file selected.';
  2469.         } else if (g.BOARD === 'f' && filetag === '9999') {
  2470.           err = 'Invalid tag specified.';
  2471.         }
  2472.       } else if (!(reply.com || reply.file)) {
  2473.         err = 'No file selected.';
  2474.       }
  2475.       if (QR.captcha.isEnabled && !err) {
  2476.         challenge = QR.captcha.img.alt;
  2477.         response = QR.captcha.input.value;
  2478.         if (!response) {
  2479.           err = 'No valid captcha.';
  2480.         } else {
  2481.           response = response.trim();
  2482.           if (!/\s/.test(response)) {
  2483.             response = "" + response + " " + response;
  2484.           }
  2485.         }
  2486.       }
  2487.       if (err) {
  2488.         QR.cooldown.auto = false;
  2489.         QR.status();
  2490.         QR.error(err);
  2491.         return;
  2492.       }
  2493.       QR.cleanError();
  2494.       QR.cooldown.auto = QR.replies.length > 1;
  2495.       if (Conf['Auto Hide QR'] && !QR.cooldown.auto) {
  2496.         QR.hide();
  2497.       }
  2498.       if (!QR.cooldown.auto && $.x('ancestor::div[@id="qr"]', d.activeElement)) {
  2499.         d.activeElement.blur();
  2500.       }
  2501.       QR.status({
  2502.         progress: '...'
  2503.       });
  2504.       post = {
  2505.         resto: threadID,
  2506.         name: reply.name,
  2507.         email: reply.email,
  2508.         sub: reply.sub,
  2509.         com: reply.com,
  2510.         upfile: reply.file,
  2511.         filetag: filetag,
  2512.         spoiler: reply.spoiler,
  2513.         textonly: textOnly,
  2514.         mode: 'regist',
  2515.         pwd: (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value,
  2516.         recaptcha_challenge_field: challenge,
  2517.         recaptcha_response_field: response
  2518.       };
  2519.       callbacks = {
  2520.         onload: function() {
  2521.           return QR.response(this.response);
  2522.         },
  2523.         onerror: function() {
  2524.           QR.cooldown.auto = false;
  2525.           QR.status();
  2526.           return QR.error($.el('a', {
  2527.             href: '//www.4chan.org/banned',
  2528.             target: '_blank',
  2529.             textContent: 'Connection error, or you are banned.'
  2530.           }));
  2531.         }
  2532.       };
  2533.       opts = {
  2534.         form: $.formData(post),
  2535.         upCallbacks: {
  2536.           onload: function() {
  2537.             return QR.status({
  2538.               progress: '...'
  2539.             });
  2540.           },
  2541.           onprogress: function(e) {
  2542.             return QR.status({
  2543.               progress: "" + (Math.round(e.loaded / e.total * 100)) + "%"
  2544.             });
  2545.           }
  2546.         }
  2547.       };
  2548.       return QR.ajax = $.ajax($.id('postForm').parentNode.action, callbacks, opts);
  2549.     },
  2550.     response: function(html) {
  2551.       var ban, board, doc, err, m, persona, postID, reply, threadID, _, _ref, _ref1;
  2552.       doc = d.implementation.createHTMLDocument('');
  2553.       doc.documentElement.innerHTML = html;
  2554.       if (ban = $('.banType', doc)) {
  2555.         board = $('.board', doc).innerHTML;
  2556.         err = $.el('span', {
  2557.           innerHTML: ban.textContent.toLowerCase() === 'banned' ? ("You are banned on " + board + "! ;_;<br>") + "Click <a href=//www.4chan.org/banned target=_blank>here</a> to see the reason." : ("You were issued a warning on " + board + " as " + ($('.nameBlock', doc).innerHTML) + ".<br>") + ("Reason: " + ($('.reason', doc).innerHTML))
  2558.         });
  2559.       } else if (err = doc.getElementById('errmsg')) {
  2560.         if ((_ref = $('a', err)) != null) {
  2561.           _ref.target = '_blank';
  2562.         }
  2563.       } else if (doc.title !== 'Post successful!') {
  2564.         err = 'Connection error with sys.4chan.org.';
  2565.       }
  2566.       if (err) {
  2567.         if (/captcha|verification/i.test(err.textContent) || err === 'Connection error with sys.4chan.org.') {
  2568.           if (/mistyped/i.test(err.textContent)) {
  2569.             err = 'Error: You seem to have mistyped the CAPTCHA.';
  2570.           }
  2571.           QR.cooldown.auto = false;
  2572.           QR.cooldown.set({
  2573.             delay: 2
  2574.           });
  2575.         } else if (err.textContent && (m = err.textContent.match(/wait\s+(\d+)\s+second/i))) {
  2576.           QR.cooldown.auto = !QR.captcha.isEnabled;
  2577.           QR.cooldown.set({
  2578.             delay: parseInt(m[1])
  2579.           });
  2580.         } else {
  2581.           QR.cooldown.auto = false;
  2582.         }
  2583.         QR.status();
  2584.         QR.error(err);
  2585.         return;
  2586.       }
  2587.       reply = QR.replies[0];
  2588.       persona = $.get('QR.persona', {});
  2589.       persona = {
  2590.         name: reply.name,
  2591.         email: /^sage$/.test(reply.email) ? persona.email : reply.email,
  2592.         sub: Conf['Remember Subject'] ? reply.sub : null
  2593.       };
  2594.       $.set('QR.persona', persona);
  2595.       _ref1 = doc.body.lastChild.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref1[0], threadID = _ref1[1], postID = _ref1[2];
  2596.       $.event(QR.el, new CustomEvent('QRPostSuccessful', {
  2597.         bubbles: true,
  2598.         detail: {
  2599.           threadID: threadID,
  2600.           postID: postID
  2601.         }
  2602.       }));
  2603.       QR.cooldown.set({
  2604.         post: reply,
  2605.         isReply: threadID !== '0'
  2606.       });
  2607.       if (threadID === '0') {
  2608.         location.pathname = "/" + g.BOARD + "/thread/" + postID;
  2609.       } else {
  2610.         QR.cooldown.auto = QR.replies.length > 1;
  2611.         if (Conf['Open Reply in New Tab'] && !g.REPLY && !QR.cooldown.auto) {
  2612.           $.open("//boards.4chan.org/" + g.BOARD + "/thread/" + threadID + "#p" + postID);
  2613.         }
  2614.       }
  2615.       if (Conf['Persistent QR'] || QR.cooldown.auto) {
  2616.         reply.rm();
  2617.       } else {
  2618.         QR.close();
  2619.       }
  2620.       QR.status();
  2621.       return QR.resetFileInput();
  2622.     },
  2623.     abort: function() {
  2624.       var _ref;
  2625.       if ((_ref = QR.ajax) != null) {
  2626.         _ref.abort();
  2627.       }
  2628.       delete QR.ajax;
  2629.       return QR.status();
  2630.     }
  2631.   };
  2632.  
  2633.   Options = {
  2634.     init: function() {
  2635.       return $.ready(Options.initReady);
  2636.     },
  2637.     initReady: function() {
  2638.       var a, setting, settings, _i, _len, _ref;
  2639.       _ref = ['navtopright', 'navbotright'];
  2640.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  2641.         settings = _ref[_i];
  2642.         a = $.el('a', {
  2643.           href: 'javascript:;',
  2644.           className: 'settingsWindowLink',
  2645.           textContent: '4chan X Settings'
  2646.         });
  2647.         $.on(a, 'click', Options.dialog);
  2648.         setting = $.id(settings);
  2649.         if (Conf['Disable 4chan\'s extension']) {
  2650.           $.replace(setting.firstElementChild, a);
  2651.         } else {
  2652.           $.prepend(setting, [$.tn('['), a, $.tn('] ')]);
  2653.         }
  2654.       }
  2655.       if (!$.get('firstrun')) {
  2656.         $.set('firstrun', true);
  2657.         if (!Favicon.el) {
  2658.           Favicon.init();
  2659.         }
  2660.         return Options.dialog();
  2661.       }
  2662.     },
  2663.     dialog: function() {
  2664.       var arr, back, checked, description, dialog, favicon, fileInfo, filter, hiddenNum, hiddenThreads, indicator, indicators, input, key, li, obj, overlay, sauce, time, tr, ul, _i, _len, _ref, _ref1, _ref2;
  2665.       dialog = $.el('div', {
  2666.         id: 'options',
  2667.         className: 'reply dialog',
  2668.         innerHTML: '<div id=optionsbar>\
  2669.  <div id=credits>\
  2670.    <a target=_blank href=http://mayhemydg.github.io/4chan-x/>4chan X</a>\
  2671.    | <a target=_blank href=https://raw.github.com/mayhemydg/4chan-x/master/changelog>' + Main.version + '</a>\
  2672.    | <a target=_blank href=http://mayhemydg.github.io/4chan-x/#bug-report>Issues</a>\
  2673.  </div>\
  2674.  <div>\
  2675.    <label for=main_tab>Main</label>\
  2676.    | <label for=filter_tab>Filter</label>\
  2677.    | <label for=sauces_tab>Sauce</label>\
  2678.    | <label for=rice_tab>Rice</label>\
  2679.    | <label for=keybinds_tab>Keybinds</label>\
  2680.  </div>\
  2681. </div>\
  2682. <hr>\
  2683. <div id=content>\
  2684.  <input type=radio name=tab hidden id=main_tab checked>\
  2685.  <div>\
  2686.    <div class=imp-exp>\
  2687.      <button class=export>Export settings</button>\
  2688.      <button class=import>Import settings</button>\
  2689.      <input type=file style="visibility:hidden">\
  2690.    </div>\
  2691.    <p class=imp-exp-result></p>\
  2692.  </div>\
  2693.  <input type=radio name=tab hidden id=sauces_tab>\
  2694.  <div>\
  2695.    <div class=warning><code>Sauce</code> is disabled.</div>\
  2696.    Lines starting with a <code>#</code> will be ignored.<br>\
  2697.    You can specify a certain display text by appending <code>;text:[text]</code> to the url.\
  2698.    <ul>These parameters will be replaced by their corresponding values:\
  2699.      <li>$1: Thumbnail url.</li>\
  2700.      <li>$2: Full image url.</li>\
  2701.      <li>$3: MD5 hash.</li>\
  2702.      <li>$4: Current board.</li>\
  2703.    </ul>\
  2704.    <textarea name=sauces id=sauces class=field></textarea>\
  2705.  </div>\
  2706.  <input type=radio name=tab hidden id=filter_tab>\
  2707.  <div>\
  2708.    <div class=warning><code>Filter</code> is disabled.</div>\
  2709.    <select name=filter>\
  2710.      <option value=guide>Guide</option>\
  2711.      <option value=name>Name</option>\
  2712.      <option value=uniqueid>Unique ID</option>\
  2713.      <option value=tripcode>Tripcode</option>\
  2714.      <option value=mod>Admin/Mod</option>\
  2715.      <option value=email>E-mail</option>\
  2716.      <option value=subject>Subject</option>\
  2717.      <option value=comment>Comment</option>\
  2718.      <option value=country>Country</option>\
  2719.      <option value=filename>Filename</option>\
  2720.      <option value=dimensions>Image dimensions</option>\
  2721.      <option value=filesize>Filesize</option>\
  2722.      <option value=md5>Image MD5 (uses exact string matching, not regular expressions)</option>\
  2723.    </select>\
  2724.  </div>\
  2725.  <input type=radio name=tab hidden id=rice_tab>\
  2726.  <div>\
  2727.    <div class=warning><code>Quote Backlinks</code> are disabled.</div>\
  2728.    <ul>\
  2729.      Backlink formatting\
  2730.      <li><input name=backlink class=field> : <span id=backlinkPreview></span></li>\
  2731.    </ul>\
  2732.    <div class=warning><code>Time Formatting</code> is disabled.</div>\
  2733.    <ul>\
  2734.      Time formatting\
  2735.      <li><input name=time class=field> : <span id=timePreview></span></li>\
  2736.      <li>Supported <a href=http://en.wikipedia.org/wiki/Date_%28Unix%29#Formatting>format specifiers</a>:</li>\
  2737.      <li>Day: %a, %A, %d, %e</li>\
  2738.      <li>Month: %m, %b, %B</li>\
  2739.      <li>Year: %y</li>\
  2740.      <li>Hour: %k, %H, %l (lowercase L), %I (uppercase i), %p, %P</li>\
  2741.      <li>Minutes: %M</li>\
  2742.      <li>Seconds: %S</li>\
  2743.    </ul>\
  2744.    <div class=warning><code>File Info Formatting</code> is disabled.</div>\
  2745.    <ul>\
  2746.      File Info Formatting\
  2747.      <li><input name=fileInfo class=field> : <span id=fileInfoPreview class=fileText></span></li>\
  2748.      <li>Link: %l (lowercase L, truncated), %L (untruncated), %t (Unix timestamp)</li>\
  2749.      <li>Original file name: %n (truncated), %N (untruncated), %T (Unix timestamp)</li>\
  2750.      <li>Spoiler indicator: %p</li>\
  2751.      <li>Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)</li>\
  2752.      <li>Resolution: %r (Displays PDF on /po/, for PDFs)</li>\
  2753.    </ul>\
  2754.    <div class=warning><code>Unread Favicon</code> is disabled.</div>\
  2755.    Unread favicons<br>\
  2756.    <select name=favicon>\
  2757.      <option value=ferongr>ferongr</option>\
  2758.      <option value=xat->xat-</option>\
  2759.      <option value=Mayhem>Mayhem</option>\
  2760.      <option value=Original>Original</option>\
  2761.    </select>\
  2762.    <span></span>\
  2763.  </div>\
  2764.  <input type=radio name=tab hidden id=keybinds_tab>\
  2765.  <div>\
  2766.    <div class=warning><code>Keybinds</code> are disabled.</div>\
  2767.    <div>Allowed keys: Ctrl, Alt, Meta, a-z, A-Z, 0-9, Up, Down, Right, Left.</div>\
  2768.    <table><tbody>\
  2769.      <tr><th>Actions</th><th>Keybinds</th></tr>\
  2770.    </tbody></table>\
  2771.  </div>\
  2772. </div>'
  2773.       });
  2774.       $.on($('#main_tab + div .export', dialog), 'click', Options["export"]);
  2775.       $.on($('#main_tab + div .import', dialog), 'click', Options["import"]);
  2776.       $.on($('#main_tab + div input', dialog), 'change', Options.onImport);
  2777.       _ref = Config.main;
  2778.       for (key in _ref) {
  2779.         obj = _ref[key];
  2780.         ul = $.el('ul', {
  2781.           textContent: key
  2782.         });
  2783.         for (key in obj) {
  2784.           arr = obj[key];
  2785.           checked = $.get(key, Conf[key]) ? 'checked' : '';
  2786.           description = arr[1];
  2787.           li = $.el('li', {
  2788.             innerHTML: "<label><input type=checkbox name=\"" + key + "\" " + checked + ">" + key + "</label><span class=description>: " + description + "</span>"
  2789.           });
  2790.           $.on($('input', li), 'click', $.cb.checked);
  2791.           $.add(ul, li);
  2792.         }
  2793.         $.add($('#main_tab + div', dialog), ul);
  2794.       }
  2795.       hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {});
  2796.       hiddenNum = Object.keys(g.hiddenReplies).length + Object.keys(hiddenThreads).length;
  2797.       li = $.el('li', {
  2798.         innerHTML: "<button>hidden: " + hiddenNum + "</button> <span class=description>: Forget all hidden posts. Useful if you accidentally hide a post and have \"Show Stubs\" disabled."
  2799.       });
  2800.       $.on($('button', li), 'click', Options.clearHidden);
  2801.       $.add($('ul:nth-child(2)', dialog), li);
  2802.       filter = $('select[name=filter]', dialog);
  2803.       $.on(filter, 'change', Options.filter);
  2804.       sauce = $('#sauces', dialog);
  2805.       sauce.value = $.get(sauce.name, Conf[sauce.name]);
  2806.       $.on(sauce, 'change', $.cb.value);
  2807.       (back = $('[name=backlink]', dialog)).value = $.get('backlink', Conf['backlink']);
  2808.       (time = $('[name=time]', dialog)).value = $.get('time', Conf['time']);
  2809.       (fileInfo = $('[name=fileInfo]', dialog)).value = $.get('fileInfo', Conf['fileInfo']);
  2810.       $.on(back, 'input', $.cb.value);
  2811.       $.on(back, 'input', Options.backlink);
  2812.       $.on(time, 'input', $.cb.value);
  2813.       $.on(time, 'input', Options.time);
  2814.       $.on(fileInfo, 'input', $.cb.value);
  2815.       $.on(fileInfo, 'input', Options.fileInfo);
  2816.       favicon = $('select[name=favicon]', dialog);
  2817.       favicon.value = $.get('favicon', Conf['favicon']);
  2818.       $.on(favicon, 'change', $.cb.value);
  2819.       $.on(favicon, 'change', Options.favicon);
  2820.       _ref1 = Config.hotkeys;
  2821.       for (key in _ref1) {
  2822.         arr = _ref1[key];
  2823.         tr = $.el('tr', {
  2824.           innerHTML: "<td>" + arr[1] + "</td><td><input name=" + key + " class=field></td>"
  2825.         });
  2826.         input = $('input', tr);
  2827.         input.value = $.get(key, Conf[key]);
  2828.         $.on(input, 'keydown', Options.keybind);
  2829.         $.add($('#keybinds_tab + div tbody', dialog), tr);
  2830.       }
  2831.       indicators = {};
  2832.       _ref2 = $$('.warning', dialog);
  2833.       for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
  2834.         indicator = _ref2[_i];
  2835.         key = indicator.firstChild.textContent;
  2836.         indicator.hidden = $.get(key, Conf[key]);
  2837.         indicators[key] = indicator;
  2838.         $.on($("[name='" + key + "']", dialog), 'click', function() {
  2839.           return indicators[this.name].hidden = this.checked;
  2840.         });
  2841.       }
  2842.       overlay = $.el('div', {
  2843.         id: 'overlay'
  2844.       });
  2845.       $.on(overlay, 'click', Options.close);
  2846.       $.on(dialog, 'click', function(e) {
  2847.         return e.stopPropagation();
  2848.       });
  2849.       $.add(overlay, dialog);
  2850.       $.add(d.body, overlay);
  2851.       d.body.style.setProperty('width', "" + d.body.clientWidth + "px", null);
  2852.       $.addClass(d.body, 'unscroll');
  2853.       Options.filter.call(filter);
  2854.       Options.backlink.call(back);
  2855.       Options.time.call(time);
  2856.       Options.fileInfo.call(fileInfo);
  2857.       return Options.favicon.call(favicon);
  2858.     },
  2859.     close: function() {
  2860.       $.rm(this);
  2861.       d.body.style.removeProperty('width');
  2862.       return $.rmClass(d.body, 'unscroll');
  2863.     },
  2864.     clearHidden: function() {
  2865.       $["delete"]("hiddenReplies/" + g.BOARD + "/");
  2866.       $["delete"]("hiddenThreads/" + g.BOARD + "/");
  2867.       this.textContent = "hidden: 0";
  2868.       return g.hiddenReplies = {};
  2869.     },
  2870.     keybind: function(e) {
  2871.       var key;
  2872.       if (e.keyCode === 9) {
  2873.         return;
  2874.       }
  2875.       e.preventDefault();
  2876.       e.stopPropagation();
  2877.       if ((key = Keybinds.keyCode(e)) == null) {
  2878.         return;
  2879.       }
  2880.       this.value = key;
  2881.       return $.cb.value.call(this);
  2882.     },
  2883.     filter: function() {
  2884.       var el, name, ta;
  2885.       el = this.nextSibling;
  2886.       if ((name = this.value) !== 'guide') {
  2887.         ta = $.el('textarea', {
  2888.           name: name,
  2889.           className: 'field',
  2890.           value: $.get(name, Conf[name])
  2891.         });
  2892.         $.on(ta, 'change', $.cb.value);
  2893.         $.replace(el, ta);
  2894.         return;
  2895.       }
  2896.       if (el) {
  2897.         $.rm(el);
  2898.       }
  2899.       return $.after(this, $.el('article', {
  2900.         innerHTML: '<p>Use <a href=https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions>regular expressions</a>, one per line.<br>\
  2901.  Lines starting with a <code>#</code> will be ignored.<br>\
  2902.  For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.</p>\
  2903.  <ul>You can use these settings with each regular expression, separate them with semicolons:\
  2904.    <li>\
  2905.      Per boards, separate them with commas. It is global if not specified.<br>\
  2906.      For example: <code>boards:a,jp;</code>.\
  2907.    </li>\
  2908.    <li>\
  2909.      Filter OPs only along with their threads (`only`), replies only (`no`, this is default), or both (`yes`).<br>\
  2910.      For example: <code>op:only;</code>, <code>op:no;</code> or <code>op:yes;</code>.\
  2911.    </li>\
  2912.    <li>\
  2913.      Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).<br>\
  2914.      For example: <code>stub:yes;</code> or <code>stub:no;</code>.\
  2915.    </li>\
  2916.    <li>\
  2917.      Highlight instead of hiding. You can specify a class name to use with a userstyle.<br>\
  2918.      For example: <code>highlight;</code> or <code>highlight:wallpaper;</code>.\
  2919.    </li>\
  2920.    <li>\
  2921.      Highlighted OPs will have their threads put on top of board pages by default.<br>\
  2922.      For example: <code>top:yes;</code> or <code>top:no;</code>.\
  2923.    </li>\
  2924.  </ul>'
  2925.       }));
  2926.     },
  2927.     time: function() {
  2928.       Time.foo();
  2929.       Time.date = new Date();
  2930.       return $.id('timePreview').textContent = Time.funk(Time);
  2931.     },
  2932.     backlink: function() {
  2933.       return $.id('backlinkPreview').textContent = Conf['backlink'].replace(/%id/, '123456789');
  2934.     },
  2935.     fileInfo: function() {
  2936.       FileInfo.data = {
  2937.         link: '//i.4cdn.org/g/src/1334437723720.jpg',
  2938.         spoiler: true,
  2939.         size: '276',
  2940.         unit: 'KB',
  2941.         resolution: '1280x720',
  2942.         fullname: 'd9bb2efc98dd0df141a94399ff5880b7.jpg',
  2943.         shortname: 'd9bb2efc98dd0df141a94399ff5880(...).jpg'
  2944.       };
  2945.       FileInfo.setFormats();
  2946.       return $.id('fileInfoPreview').innerHTML = FileInfo.funk(FileInfo);
  2947.     },
  2948.     favicon: function() {
  2949.       Favicon["switch"]();
  2950.       Unread.update(true);
  2951.       return this.nextElementSibling.innerHTML = "<img src=" + Favicon.unreadSFW + "> <img src=" + Favicon.unreadNSFW + "> <img src=" + Favicon.unreadDead + ">";
  2952.     },
  2953.     "export": function() {
  2954.       var a, data, now, output;
  2955.       now = Date.now();
  2956.       data = {
  2957.         version: Main.version,
  2958.         date: now,
  2959.         Conf: Conf,
  2960.         WatchedThreads: $.get('watched', {})
  2961.       };
  2962.       a = $.el('a', {
  2963.         className: 'warning',
  2964.         textContent: 'Save me!',
  2965.         download: "4chan X v" + Main.version + "-" + now + ".json",
  2966.         href: "data:application/json;base64," + (btoa(unescape(encodeURIComponent(JSON.stringify(data))))),
  2967.         target: '_blank'
  2968.       });
  2969.       if ($.engine !== 'gecko') {
  2970.         a.click();
  2971.         return;
  2972.       }
  2973.       output = this.parentNode.nextElementSibling;
  2974.       output.innerHTML = null;
  2975.       return $.add(output, a);
  2976.     },
  2977.     "import": function() {
  2978.       return this.nextElementSibling.click();
  2979.     },
  2980.     onImport: function() {
  2981.       var file, output, reader;
  2982.       if (!(file = this.files[0])) {
  2983.         return;
  2984.       }
  2985.       output = this.parentNode.nextElementSibling;
  2986.       if (!confirm('Your current settings will be entirely overwritten, are you sure?')) {
  2987.         output.textContent = 'Import aborted.';
  2988.         return;
  2989.       }
  2990.       reader = new FileReader();
  2991.       reader.onload = function(e) {
  2992.         var data, err;
  2993.         try {
  2994.           data = JSON.parse(e.target.result);
  2995.           Options.loadSettings(data);
  2996.           if (confirm('Import successful. Refresh now?')) {
  2997.             return window.location.reload();
  2998.           }
  2999.         } catch (_error) {
  3000.           err = _error;
  3001.           return output.textContent = 'Import failed due to an error.';
  3002.         }
  3003.       };
  3004.       return reader.readAsText(file);
  3005.     },
  3006.     loadSettings: function(data) {
  3007.       var key, val, _ref;
  3008.       _ref = data.Conf;
  3009.       for (key in _ref) {
  3010.         val = _ref[key];
  3011.         $.set(key, val);
  3012.       }
  3013.       return $.set('watched', data.WatchedThreads);
  3014.     }
  3015.   };
  3016.  
  3017.   Updater = {
  3018.     init: function() {
  3019.       var checkbox, checked, dialog, html, input, name, title, _i, _len, _ref;
  3020.       html = '<div class=move><span id=count></span> <span id=timer></span></div>';
  3021.       checkbox = Config.updater.checkbox;
  3022.       for (name in checkbox) {
  3023.         title = checkbox[name][1];
  3024.         checked = Conf[name] ? 'checked' : '';
  3025.         html += "<div><label title='" + title + "'>" + name + "<input name='" + name + "' type=checkbox " + checked + "></label></div>";
  3026.       }
  3027.       checked = Conf['Auto Update'] ? 'checked' : '';
  3028.       html += "      <div><label title='Controls whether *this* thread automatically updates or not'>Auto Update This<input name='Auto Update This' type=checkbox " + checked + "></label></div>      <div><label>Interval (s)<input type=number name=Interval class=field min=5></label></div>      <div><input value='Update Now' type=button name='Update Now'></div>";
  3029.       dialog = UI.dialog('updater', 'bottom: 0; right: 0;', html);
  3030.       this.count = $('#count', dialog);
  3031.       this.timer = $('#timer', dialog);
  3032.       this.thread = $.id("t" + g.THREAD_ID);
  3033.       this.unsuccessfulFetchCount = 0;
  3034.       this.lastModified = '0';
  3035.       _ref = $$('input', dialog);
  3036.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  3037.         input = _ref[_i];
  3038.         if (input.type === 'checkbox') {
  3039.           $.on(input, 'click', $.cb.checked);
  3040.         }
  3041.         switch (input.name) {
  3042.           case 'Scroll BG':
  3043.             $.on(input, 'click', this.cb.scrollBG);
  3044.             this.cb.scrollBG.call(input);
  3045.             break;
  3046.           case 'Verbose':
  3047.             $.on(input, 'click', this.cb.verbose);
  3048.             this.cb.verbose.call(input);
  3049.             break;
  3050.           case 'Auto Update This':
  3051.             $.on(input, 'click', this.cb.autoUpdate);
  3052.             this.cb.autoUpdate.call(input);
  3053.             break;
  3054.           case 'Interval':
  3055.             input.value = Conf['Interval'];
  3056.             $.on(input, 'change', this.cb.interval);
  3057.             this.cb.interval.call(input);
  3058.             break;
  3059.           case 'Update Now':
  3060.             $.on(input, 'click', this.update);
  3061.         }
  3062.       }
  3063.       $.add(d.body, dialog);
  3064.       $.on(d, 'QRPostSuccessful', this.cb.post);
  3065.       return $.on(d, 'visibilitychange', this.cb.visibility);
  3066.     },
  3067.     /*
  3068.     http://freesound.org/people/pierrecartoons1979/sounds/90112/
  3069.     cc-by-nc-3.0
  3070.     */
  3071.  
  3072.     audio: $.el('audio', {
  3073.       src: 'data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA'
  3074.     }),
  3075.     cb: {
  3076.       post: function() {
  3077.         if (!Conf['Auto Update This']) {
  3078.           return;
  3079.         }
  3080.         Updater.unsuccessfulFetchCount = 0;
  3081.         return setTimeout(Updater.update, 500);
  3082.       },
  3083.       visibility: function() {
  3084.         if (d.hidden) {
  3085.           return;
  3086.         }
  3087.         Updater.unsuccessfulFetchCount = 0;
  3088.         if (Updater.timer.textContent < -Conf['Interval']) {
  3089.           return Updater.set('timer', -Updater.getInterval());
  3090.         }
  3091.       },
  3092.       interval: function() {
  3093.         var val;
  3094.         val = parseInt(this.value, 10);
  3095.         this.value = val > 5 ? val : 5;
  3096.         $.cb.value.call(this);
  3097.         return Updater.set('timer', -Updater.getInterval());
  3098.       },
  3099.       verbose: function() {
  3100.         if (Conf['Verbose']) {
  3101.           Updater.set('count', '+0');
  3102.           return Updater.timer.hidden = false;
  3103.         } else {
  3104.           Updater.set('count', 'Thread Updater');
  3105.           Updater.count.className = '';
  3106.           return Updater.timer.hidden = true;
  3107.         }
  3108.       },
  3109.       autoUpdate: function() {
  3110.         if (Conf['Auto Update This'] = this.checked) {
  3111.           return Updater.timeoutID = setTimeout(Updater.timeout, 1000);
  3112.         } else {
  3113.           return clearTimeout(Updater.timeoutID);
  3114.         }
  3115.       },
  3116.       scrollBG: function() {
  3117.         return Updater.scrollBG = this.checked ? function() {
  3118.           return true;
  3119.         } : function() {
  3120.           return !d.hidden;
  3121.         };
  3122.       },
  3123.       load: function() {
  3124.         switch (this.status) {
  3125.           case 404:
  3126.             Updater.set('timer', '');
  3127.             Updater.set('count', 404);
  3128.             Updater.count.className = 'warning';
  3129.             clearTimeout(Updater.timeoutID);
  3130.             g.dead = true;
  3131.             if (Conf['Unread Count']) {
  3132.               Unread.title = Unread.title.match(/^.+-/)[0] + ' 404';
  3133.             } else {
  3134.               d.title = d.title.match(/^.+-/)[0] + ' 404';
  3135.             }
  3136.             Unread.update(true);
  3137.             QR.abort();
  3138.             break;
  3139.           case 0:
  3140.           case 304:
  3141.             /*
  3142.             Status Code 304: Not modified
  3143.             By sending the `If-Modified-Since` header we get a proper status code, and no response.
  3144.             This saves bandwidth for both the user and the servers and avoid unnecessary computation.
  3145.             */
  3146.  
  3147.             Updater.unsuccessfulFetchCount++;
  3148.             Updater.set('timer', -Updater.getInterval());
  3149.             if (Conf['Verbose']) {
  3150.               Updater.set('count', '+0');
  3151.               Updater.count.className = null;
  3152.             }
  3153.             break;
  3154.           case 200:
  3155.             Updater.lastModified = this.getResponseHeader('Last-Modified');
  3156.             Updater.cb.update(JSON.parse(this.response).posts);
  3157.             Updater.set('timer', -Updater.getInterval());
  3158.             break;
  3159.           default:
  3160.             Updater.unsuccessfulFetchCount++;
  3161.             Updater.set('timer', -Updater.getInterval());
  3162.             if (Conf['Verbose']) {
  3163.               Updater.set('count', this.statusText);
  3164.               Updater.count.className = 'warning';
  3165.             }
  3166.         }
  3167.         return delete Updater.request;
  3168.       },
  3169.       update: function(posts) {
  3170.         var count, id, lastPost, nodes, post, scroll, spoilerRange, _i, _len, _ref;
  3171.         if (spoilerRange = posts[0].custom_spoiler) {
  3172.           Build.spoilerRange[g.BOARD] = spoilerRange;
  3173.         }
  3174.         lastPost = Updater.thread.lastElementChild;
  3175.         id = +lastPost.id.slice(2);
  3176.         nodes = [];
  3177.         _ref = posts.reverse();
  3178.         for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  3179.           post = _ref[_i];
  3180.           if (post.no <= id) {
  3181.             break;
  3182.           }
  3183.           nodes.push(Build.postFromObject(post, g.BOARD));
  3184.         }
  3185.         count = nodes.length;
  3186.         if (Conf['Verbose']) {
  3187.           Updater.set('count', "+" + count);
  3188.           Updater.count.className = count ? 'new' : null;
  3189.         }
  3190.         if (count) {
  3191.           if (Conf['Beep'] && d.hidden && (Unread.replies.length === 0)) {
  3192.             Updater.audio.play();
  3193.           }
  3194.           Updater.unsuccessfulFetchCount = 0;
  3195.         } else {
  3196.           Updater.unsuccessfulFetchCount++;
  3197.           return;
  3198.         }
  3199.         scroll = Conf['Scrolling'] && Updater.scrollBG() && lastPost.getBoundingClientRect().bottom - d.documentElement.clientHeight < 25;
  3200.         $.add(Updater.thread, nodes.reverse());
  3201.         if (scroll) {
  3202.           return nodes[0].scrollIntoView();
  3203.         }
  3204.       }
  3205.     },
  3206.     set: function(name, text) {
  3207.       var el, node;
  3208.       el = Updater[name];
  3209.       if (node = el.firstChild) {
  3210.         return node.data = text;
  3211.       } else {
  3212.         return el.textContent = text;
  3213.       }
  3214.     },
  3215.     getInterval: function() {
  3216.       return Conf['Interval'];
  3217.     },
  3218.     timeout: function() {
  3219.       var n;
  3220.       Updater.timeoutID = setTimeout(Updater.timeout, 1000);
  3221.       n = 1 + Number(Updater.timer.firstChild.data);
  3222.       if (n === 0) {
  3223.         return Updater.update();
  3224.       } else if (n >= Updater.getInterval()) {
  3225.         Updater.unsuccessfulFetchCount++;
  3226.         Updater.set('count', 'Retry');
  3227.         Updater.count.className = null;
  3228.         return Updater.update();
  3229.       } else {
  3230.         return Updater.set('timer', n);
  3231.       }
  3232.     },
  3233.     update: function() {
  3234.       var request, url;
  3235.       Updater.set('timer', 0);
  3236.       request = Updater.request;
  3237.       if (request) {
  3238.         request.onloadend = null;
  3239.         request.abort();
  3240.       }
  3241.       url = "//a.4cdn.org/" + g.BOARD + "/thread/" + g.THREAD_ID + ".json";
  3242.       return Updater.request = $.ajax(url, {
  3243.         onloadend: Updater.cb.load
  3244.       }, {
  3245.         headers: {
  3246.           'If-Modified-Since': Updater.lastModified
  3247.         }
  3248.       });
  3249.     }
  3250.   };
  3251.  
  3252.   Watcher = {
  3253.     init: function() {
  3254.       var favicon, html, input, _i, _len, _ref;
  3255.       html = '<div class=move>Thread Watcher</div>';
  3256.       this.dialog = UI.dialog('watcher', 'top: 50px; left: 0px;', html);
  3257.       $.add(d.body, this.dialog);
  3258.       _ref = $$('.op input');
  3259.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  3260.         input = _ref[_i];
  3261.         favicon = $.el('img', {
  3262.           className: 'favicon'
  3263.         });
  3264.         $.on(favicon, 'click', this.cb.toggle);
  3265.         $.before(input, favicon);
  3266.       }
  3267.       if (g.THREAD_ID === $.get('autoWatch', 0)) {
  3268.         this.watch(g.THREAD_ID);
  3269.         $["delete"]('autoWatch');
  3270.       } else {
  3271.         this.refresh();
  3272.       }
  3273.       $.on(d, 'QRPostSuccessful', this.cb.post);
  3274.       return $.sync('watched', this.refresh);
  3275.     },
  3276.     refresh: function(watched) {
  3277.       var board, div, favicon, id, link, nodes, props, watchedBoard, x, _i, _j, _len, _len1, _ref, _ref1, _ref2;
  3278.       watched || (watched = $.get('watched', {}));
  3279.       nodes = [];
  3280.       for (board in watched) {
  3281.         _ref = watched[board];
  3282.         for (id in _ref) {
  3283.           props = _ref[id];
  3284.           x = $.el('a', {
  3285.             textContent: '×',
  3286.             href: 'javascript:;'
  3287.           });
  3288.           $.on(x, 'click', Watcher.cb.x);
  3289.           link = $.el('a', props);
  3290.           link.title = link.textContent;
  3291.           div = $.el('div');
  3292.           $.add(div, [x, $.tn(' '), link]);
  3293.           nodes.push(div);
  3294.         }
  3295.       }
  3296.       _ref1 = $$('div:not(.move)', Watcher.dialog);
  3297.       for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
  3298.         div = _ref1[_i];
  3299.         $.rm(div);
  3300.       }
  3301.       $.add(Watcher.dialog, nodes);
  3302.       watchedBoard = watched[g.BOARD] || {};
  3303.       _ref2 = $$('.favicon');
  3304.       for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
  3305.         favicon = _ref2[_j];
  3306.         id = favicon.nextSibling.name;
  3307.         if (id in watchedBoard) {
  3308.           favicon.src = Favicon["default"];
  3309.         } else {
  3310.           favicon.src = Favicon.empty;
  3311.         }
  3312.       }
  3313.     },
  3314.     cb: {
  3315.       toggle: function() {
  3316.         return Watcher.toggle(this.parentNode);
  3317.       },
  3318.       x: function() {
  3319.         var thread;
  3320.         thread = this.nextElementSibling.pathname.split('/');
  3321.         return Watcher.unwatch(thread[3], thread[1]);
  3322.       },
  3323.       post: function(e) {
  3324.         var postID, threadID, _ref;
  3325.         _ref = e.detail, postID = _ref.postID, threadID = _ref.threadID;
  3326.         if (threadID === '0') {
  3327.           if (Conf['Auto Watch']) {
  3328.             return $.set('autoWatch', postID);
  3329.           }
  3330.         } else if (Conf['Auto Watch Reply']) {
  3331.           return Watcher.watch(threadID);
  3332.         }
  3333.       }
  3334.     },
  3335.     toggle: function(thread) {
  3336.       var id;
  3337.       id = $('.favicon + input', thread).name;
  3338.       return Watcher.watch(id) || Watcher.unwatch(id, g.BOARD);
  3339.     },
  3340.     unwatch: function(id, board) {
  3341.       var watched;
  3342.       watched = $.get('watched', {});
  3343.       delete watched[board][id];
  3344.       $.set('watched', watched);
  3345.       return Watcher.refresh();
  3346.     },
  3347.     watch: function(id) {
  3348.       var thread, watched, _name;
  3349.       thread = $.id("t" + id);
  3350.       if ($('.favicon', thread).src === Favicon["default"]) {
  3351.         return false;
  3352.       }
  3353.       watched = $.get('watched', {});
  3354.       watched[_name = g.BOARD] || (watched[_name] = {});
  3355.       watched[g.BOARD][id] = {
  3356.         href: "/" + g.BOARD + "/thread/" + id,
  3357.         textContent: Get.title(thread)
  3358.       };
  3359.       $.set('watched', watched);
  3360.       Watcher.refresh();
  3361.       return true;
  3362.     }
  3363.   };
  3364.  
  3365.   Anonymize = {
  3366.     init: function() {
  3367.       return Main.callbacks.push(this.node);
  3368.     },
  3369.     node: function(post) {
  3370.       var name, parent, trip;
  3371.       if (post.isInlined && !post.isCrosspost) {
  3372.         return;
  3373.       }
  3374.       name = $('.postInfo .name', post.el);
  3375.       name.textContent = 'Anonymous';
  3376.       if ((trip = name.nextElementSibling) && trip.className === 'postertrip') {
  3377.         $.rm(trip);
  3378.       }
  3379.       if ((parent = name.parentNode).className === 'useremail' && !/^mailto:sage$/i.test(parent.href)) {
  3380.         return $.replace(parent, name);
  3381.       }
  3382.     }
  3383.   };
  3384.  
  3385.   Sauce = {
  3386.     init: function() {
  3387.       var link, _i, _len, _ref;
  3388.       if (g.BOARD === 'f') {
  3389.         return;
  3390.       }
  3391.       this.links = [];
  3392.       _ref = Conf['sauces'].split('\n');
  3393.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  3394.         link = _ref[_i];
  3395.         if (link[0] === '#') {
  3396.           continue;
  3397.         }
  3398.         this.links.push(this.createSauceLink(link.trim()));
  3399.       }
  3400.       if (!this.links.length) {
  3401.         return;
  3402.       }
  3403.       return Main.callbacks.push(this.node);
  3404.     },
  3405.     createSauceLink: function(link) {
  3406.       var domain, el, href, m, t = "(isArchived ? img.firstChild.src : 'http://t.4cdn.org' + img.pathname.replace(/(\\d+).+$/, 'thumb$1s.jpg'))";
  3407.       link = link.replace(/(\$\d)/g, function(parameter) {
  3408.         switch (parameter) {
  3409.           case '$1':
  3410.             return "' + "+ t +" + '";
  3411.           case '$2':
  3412.             return "' + ( img.href.indexOf('webm') > -1 ? "+ t +" : img.href ) + '";
  3413.           case '$3':
  3414.             return "' + encodeURIComponent(img.firstChild.dataset.md5) + '";
  3415.           case '$4':
  3416.             return g.BOARD;
  3417.           default:
  3418.             return parameter;
  3419.         }
  3420.       });
  3421.       domain = (m = link.match(/;text:(.+)$/)) ? m[1] : link.match(/(\w+)\.\w+\//)[1];
  3422.       href = link.replace(/;text:.+$/, '');
  3423.       href = Function('img', 'isArchived', "return '" + href + "'");
  3424.       el = $.el('a', {
  3425.         target: '_blank',
  3426.         textContent: domain
  3427.       });
  3428.       return function(img, isArchived) {
  3429.         var a;
  3430.         a = el.cloneNode(true);
  3431.         a.href = href(img, isArchived);
  3432.         return a;
  3433.       };
  3434.     },
  3435.     node: function(post) {
  3436.       var img, link, nodes, _i, _len, _ref;
  3437.       img = post.img;
  3438.       if (post.isInlined && !post.isCrosspost || !img) {
  3439.         return;
  3440.       }
  3441.       img = img.parentNode;
  3442.       nodes = [];
  3443.       _ref = Sauce.links;
  3444.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  3445.         link = _ref[_i];
  3446.         nodes.push($.tn('\u00A0'), link(img, post.isArchived));
  3447.       }
  3448.       return $.add(post.fileInfo, nodes);
  3449.     }
  3450.   };
  3451.  
  3452.   RevealSpoilers = {
  3453.     init: function() {
  3454.       return Main.callbacks.push(this.node);
  3455.     },
  3456.     node: function(post) {
  3457.       var img, s;
  3458.       img = post.img;
  3459.       if (!(img && /^Spoiler/.test(img.alt)) || post.isInlined && !post.isCrosspost || post.isArchived) {
  3460.         return;
  3461.       }
  3462.       img.removeAttribute('style');
  3463.       s = img.style;
  3464.       s.maxHeight = s.maxWidth = /\bop\b/.test(post["class"]) ? '250px' : '125px';
  3465.       return img.src = "//t.4cdn.org" + (img.parentNode.pathname.replace(/(\d+).+$/, 'thumb$1s.jpg'));
  3466.     }
  3467.   };
  3468.  
  3469.   Time = {
  3470.     init: function() {
  3471.       Time.foo();
  3472.       return Main.callbacks.push(this.node);
  3473.     },
  3474.     node: function(post) {
  3475.       var node;
  3476.       if (post.isInlined && !post.isCrosspost) {
  3477.         return;
  3478.       }
  3479.       node = $('.postInfo > .dateTime', post.el);
  3480.       Time.date = new Date(node.dataset.utc * 1000);
  3481.       return node.textContent = Time.funk(Time);
  3482.     },
  3483.     foo: function() {
  3484.       var code;
  3485.       code = Conf['time'].replace(/%([A-Za-z])/g, function(s, c) {
  3486.         if (c in Time.formatters) {
  3487.           return "' + Time.formatters." + c + "() + '";
  3488.         } else {
  3489.           return s;
  3490.         }
  3491.       });
  3492.       return Time.funk = Function('Time', "return '" + code + "'");
  3493.     },
  3494.     day: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
  3495.     month: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
  3496.     zeroPad: function(n) {
  3497.       if (n < 10) {
  3498.         return '0' + n;
  3499.       } else {
  3500.         return n;
  3501.       }
  3502.     },
  3503.     formatters: {
  3504.       a: function() {
  3505.         return Time.day[Time.date.getDay()].slice(0, 3);
  3506.       },
  3507.       A: function() {
  3508.         return Time.day[Time.date.getDay()];
  3509.       },
  3510.       b: function() {
  3511.         return Time.month[Time.date.getMonth()].slice(0, 3);
  3512.       },
  3513.       B: function() {
  3514.         return Time.month[Time.date.getMonth()];
  3515.       },
  3516.       d: function() {
  3517.         return Time.zeroPad(Time.date.getDate());
  3518.       },
  3519.       e: function() {
  3520.         return Time.date.getDate();
  3521.       },
  3522.       H: function() {
  3523.         return Time.zeroPad(Time.date.getHours());
  3524.       },
  3525.       I: function() {
  3526.         return Time.zeroPad(Time.date.getHours() % 12 || 12);
  3527.       },
  3528.       k: function() {
  3529.         return Time.date.getHours();
  3530.       },
  3531.       l: function() {
  3532.         return Time.date.getHours() % 12 || 12;
  3533.       },
  3534.       m: function() {
  3535.         return Time.zeroPad(Time.date.getMonth() + 1);
  3536.       },
  3537.       M: function() {
  3538.         return Time.zeroPad(Time.date.getMinutes());
  3539.       },
  3540.       p: function() {
  3541.         if (Time.date.getHours() < 12) {
  3542.           return 'AM';
  3543.         } else {
  3544.           return 'PM';
  3545.         }
  3546.       },
  3547.       P: function() {
  3548.         if (Time.date.getHours() < 12) {
  3549.           return 'am';
  3550.         } else {
  3551.           return 'pm';
  3552.         }
  3553.       },
  3554.       S: function() {
  3555.         return Time.zeroPad(Time.date.getSeconds());
  3556.       },
  3557.       y: function() {
  3558.         return Time.date.getFullYear() - 2000;
  3559.       }
  3560.     }
  3561.   };
  3562.  
  3563.   RelativeDates = {
  3564.     INTERVAL: $.MINUTE,
  3565.     init: function() {
  3566.       Main.callbacks.push(this.node);
  3567.       return $.on(d, 'visibilitychange', this.flush);
  3568.     },
  3569.     node: function(post) {
  3570.       var dateEl, diff, utc;
  3571.       dateEl = $('.postInfo > .dateTime', post.el);
  3572.       dateEl.title = dateEl.textContent;
  3573.       utc = dateEl.dataset.utc * 1000;
  3574.       diff = Date.now() - utc;
  3575.       dateEl.textContent = RelativeDates.relative(diff);
  3576.       RelativeDates.setUpdate(dateEl, utc, diff);
  3577.       return RelativeDates.flush();
  3578.     },
  3579.     relative: function(diff) {
  3580.       var number, rounded, unit;
  3581.       unit = (number = diff / $.DAY) > 1 ? 'day' : (number = diff / $.HOUR) > 1 ? 'hour' : (number = diff / $.MINUTE) > 1 ? 'minute' : (number = diff / $.SECOND, 'second');
  3582.       rounded = Math.round(number);
  3583.       if (rounded !== 1) {
  3584.         unit += 's';
  3585.       }
  3586.       return "" + rounded + " " + unit + " ago";
  3587.     },
  3588.     stale: [],
  3589.     flush: $.debounce($.SECOND, function() {
  3590.       var now, update, _i, _len, _ref;
  3591.       if (d.hidden) {
  3592.         return;
  3593.       }
  3594.       now = Date.now();
  3595.       _ref = RelativeDates.stale;
  3596.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  3597.         update = _ref[_i];
  3598.         update(now);
  3599.       }
  3600.       RelativeDates.stale = [];
  3601.       clearTimeout(RelativeDates.timeout);
  3602.       return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL);
  3603.     }),
  3604.     setUpdate: function(dateEl, utc, diff) {
  3605.       var markStale, setOwnTimeout, update;
  3606.       setOwnTimeout = function(diff) {
  3607.         var delay;
  3608.         delay = diff < $.MINUTE ? $.SECOND - (diff + $.SECOND / 2) % $.SECOND : diff < $.HOUR ? $.MINUTE - (diff + $.MINUTE / 2) % $.MINUTE : $.HOUR - (diff + $.HOUR / 2) % $.HOUR;
  3609.         return setTimeout(markStale, delay);
  3610.       };
  3611.       update = function(now) {
  3612.         if (d.contains(dateEl)) {
  3613.           diff = now - utc;
  3614.           dateEl.textContent = RelativeDates.relative(diff);
  3615.           return setOwnTimeout(diff);
  3616.         }
  3617.       };
  3618.       markStale = function() {
  3619.         return RelativeDates.stale.push(update);
  3620.       };
  3621.       return setOwnTimeout(diff);
  3622.     }
  3623.   };
  3624.  
  3625.   FileInfo = {
  3626.     init: function() {
  3627.       if (g.BOARD === 'f') {
  3628.         return;
  3629.       }
  3630.       this.setFormats();
  3631.       return Main.callbacks.push(this.node);
  3632.     },
  3633.     node: function(post) {
  3634.       var alt, filename, node, _ref;
  3635.       if (post.isInlined && !post.isCrosspost || !post.fileInfo) {
  3636.         return;
  3637.       }
  3638.       node = post.fileInfo;
  3639.       alt = post.img.alt;
  3640.       filename = (_ref = $('a', node)) ? _ref.title || _ref.textContent : node.title;
  3641.       FileInfo.data = {
  3642.         link: post.img.parentNode.href,
  3643.         spoiler: /^Spoiler/.test(alt),
  3644.         size: alt.match(/\d+\.?\d*/)[0],
  3645.         unit: alt.match(/\w+$/)[0],
  3646.         resolution: node.textContent.match(/\d+x\d+|PDF/)[0],
  3647.         fullname: filename,
  3648.         shortname: Build.shortFilename(filename, post.ID === post.threadID)
  3649.       };
  3650.       node.setAttribute('data-filename', filename);
  3651.       return node.innerHTML = FileInfo.funk(FileInfo);
  3652.     },
  3653.     setFormats: function() {
  3654.       var code;
  3655.       code = Conf['fileInfo'].replace(/%(.)/g, function(s, c) {
  3656.         if (c in FileInfo.formatters) {
  3657.           return "' + f.formatters." + c + "() + '";
  3658.         } else {
  3659.           return s;
  3660.         }
  3661.       });
  3662.       return this.funk = Function('f', "return '" + code + "'");
  3663.     },
  3664.     convertUnit: function(unitT) {
  3665.       var i, size, unitF, units;
  3666.       size = this.data.size;
  3667.       unitF = this.data.unit;
  3668.       if (unitF !== unitT) {
  3669.         units = ['B', 'KB', 'MB'];
  3670.         i = units.indexOf(unitF) - units.indexOf(unitT);
  3671.         if (unitT === 'B') {
  3672.           unitT = 'Bytes';
  3673.         }
  3674.         if (i > 0) {
  3675.           while (i-- > 0) {
  3676.             size *= 1024;
  3677.           }
  3678.         } else if (i < 0) {
  3679.           while (i++ < 0) {
  3680.             size /= 1024;
  3681.           }
  3682.         }
  3683.         if (size < 1 && size.toString().length > size.toFixed(2).length) {
  3684.           size = size.toFixed(2);
  3685.         }
  3686.       }
  3687.       return "" + size + " " + unitT;
  3688.     },
  3689.     formatters: {
  3690.       t: function() {
  3691.         return FileInfo.data.link.match(/\d+\..+$/)[0];
  3692.       },
  3693.       T: function() {
  3694.         return "<a href=" + FileInfo.data.link + " target=_blank>" + (this.t()) + "</a>";
  3695.       },
  3696.       l: function() {
  3697.         return "<a href=" + FileInfo.data.link + " target=_blank>" + (this.n()) + "</a>";
  3698.       },
  3699.       L: function() {
  3700.         return "<a href=" + FileInfo.data.link + " target=_blank>" + (this.N()) + "</a>";
  3701.       },
  3702.       n: function() {
  3703.         if (FileInfo.data.fullname === FileInfo.data.shortname) {
  3704.           return FileInfo.data.fullname;
  3705.         } else {
  3706.           return "<span class=fntrunc>" + FileInfo.data.shortname + "</span><span class=fnfull>" + FileInfo.data.fullname + "</span>";
  3707.         }
  3708.       },
  3709.       N: function() {
  3710.         return FileInfo.data.fullname;
  3711.       },
  3712.       p: function() {
  3713.         if (FileInfo.data.spoiler) {
  3714.           return 'Spoiler, ';
  3715.         } else {
  3716.           return '';
  3717.         }
  3718.       },
  3719.       s: function() {
  3720.         return "" + FileInfo.data.size + " " + FileInfo.data.unit;
  3721.       },
  3722.       B: function() {
  3723.         return FileInfo.convertUnit('B');
  3724.       },
  3725.       K: function() {
  3726.         return FileInfo.convertUnit('KB');
  3727.       },
  3728.       M: function() {
  3729.         return FileInfo.convertUnit('MB');
  3730.       },
  3731.       r: function() {
  3732.         return FileInfo.data.resolution;
  3733.       }
  3734.     }
  3735.   };
  3736.  
  3737.   Get = {
  3738.     post: function(board, threadID, postID, root, cb) {
  3739.       var post, url;
  3740.       if (board === g.BOARD && (post = $.id("pc" + postID))) {
  3741.         $.add(root, Get.cleanPost(post.cloneNode(true)));
  3742.         return;
  3743.       }
  3744.       root.textContent = "Loading post No." + postID + "...";
  3745.       if (threadID) {
  3746.         return $.cache("//a.4cdn.org/" + board + "/thread/" + threadID + ".json", function() {
  3747.           return Get.parsePost(this, board, threadID, postID, root, cb);
  3748.         });
  3749.       } else if (url = Redirect.post(board, postID)) {
  3750.         return $.cache(url, function() {
  3751.           return Get.parseArchivedPost(this, board, postID, root, cb);
  3752.         });
  3753.       }
  3754.     },
  3755.     parsePost: function(req, board, threadID, postID, root, cb) {
  3756.       var post, posts, spoilerRange, status, url, _i, _len;
  3757.       status = req.status;
  3758.       if (status !== 200) {
  3759.         if (url = Redirect.post(board, postID)) {
  3760.           $.cache(url, function() {
  3761.             return Get.parseArchivedPost(this, board, postID, root, cb);
  3762.           });
  3763.         } else {
  3764.           $.addClass(root, 'warning');
  3765.           root.textContent = status === 404 ? "Thread No." + threadID + " 404'd." : "Error " + req.status + ": " + req.statusText + ".";
  3766.         }
  3767.         return;
  3768.       }
  3769.       posts = JSON.parse(req.response).posts;
  3770.       if (spoilerRange = posts[0].custom_spoiler) {
  3771.         Build.spoilerRange[board] = spoilerRange;
  3772.       }
  3773.       postID = +postID;
  3774.       for (_i = 0, _len = posts.length; _i < _len; _i++) {
  3775.         post = posts[_i];
  3776.         if (post.no === postID) {
  3777.           break;
  3778.         }
  3779.         if (post.no > postID) {
  3780.           if (url = Redirect.post(board, postID)) {
  3781.             $.cache(url, function() {
  3782.               return Get.parseArchivedPost(this, board, postID, root, cb);
  3783.             });
  3784.           } else {
  3785.             $.addClass(root, 'warning');
  3786.             root.textContent = "Post No." + postID + " was not found.";
  3787.           }
  3788.           return;
  3789.         }
  3790.       }
  3791.       $.replace(root.firstChild, Get.cleanPost(Build.postFromObject(post, board)));
  3792.       if (cb) {
  3793.         return cb();
  3794.       }
  3795.     },
  3796.     parseArchivedPost: function(req, board, postID, root, cb) {
  3797.       var bq, comment, data, o, _ref;
  3798.       data = JSON.parse(req.response);
  3799.       if (data.error) {
  3800.         $.addClass(root, 'warning');
  3801.         root.textContent = data.error;
  3802.         return;
  3803.       }
  3804.       bq = $.el('blockquote', {
  3805.         textContent: data.comment
  3806.       });
  3807.       bq.innerHTML = bq.innerHTML.replace(/\n|\[\/?b\]|\[\/?spoiler\]|\[\/?code\]|\[\/?moot\]|\[\/?banned\]/g, function(text) {
  3808.         switch (text) {
  3809.           case '\n':
  3810.             return '<br>';
  3811.           case '[b]':
  3812.             return '<b>';
  3813.           case '[/b]':
  3814.             return '</b>';
  3815.           case '[spoiler]':
  3816.             return '<span class=spoiler>';
  3817.           case '[/spoiler]':
  3818.             return '</span>';
  3819.           case '[code]':
  3820.             return '<pre class=prettyprint>';
  3821.           case '[/code]':
  3822.             return '</pre>';
  3823.           case '[moot]':
  3824.             return '<div style="padding:5px;margin-left:.5em;border-color:#faa;border:2px dashed rgba(255,0,0,.1);border-radius:2px">';
  3825.           case '[/moot]':
  3826.             return '</div>';
  3827.           case '[banned]':
  3828.             return '<b style="color: red;">';
  3829.           case '[/banned]':
  3830.             return '</b>';
  3831.         }
  3832.       });
  3833.       comment = bq.innerHTML.replace(/(^|>)(&gt;[^<$]*)(<|$)/g, '$1<span class=quote>$2</span>$3');
  3834.       comment = comment.replace(/((&gt;){2}(&gt;\/[a-z\d]+\/)?\d+)/g, '<span class=deadlink>$1</span>');
  3835.       o = {
  3836.         postID: postID,
  3837.         threadID: data.thread_num,
  3838.         board: board,
  3839.         name: data.name_processed,
  3840.         capcode: (function() {
  3841.           switch (data.capcode) {
  3842.             case 'M':
  3843.               return 'mod';
  3844.             case 'A':
  3845.               return 'admin';
  3846.             case 'D':
  3847.               return 'developer';
  3848.           }
  3849.         })(),
  3850.         tripcode: data.trip,
  3851.         uniqueID: data.poster_hash,
  3852.         email: data.email ? encodeURI(data.email) : '',
  3853.         subject: data.title_processed,
  3854.         flagCode: data.poster_country,
  3855.         flagName: data.poster_country_name_processed,
  3856.         date: data.fourchan_date,
  3857.         dateUTC: data.timestamp,
  3858.         comment: comment
  3859.       };
  3860.       if ((_ref = data.media) != null ? _ref.media_filename : void 0) {
  3861.         o.file = {
  3862.           name: data.media.media_filename_processed,
  3863.           timestamp: data.media.media_orig,
  3864.           url: data.media.media_link || data.media.remote_media_link,
  3865.           height: data.media.media_h,
  3866.           width: data.media.media_w,
  3867.           MD5: data.media.media_hash,
  3868.           size: data.media.media_size,
  3869.           turl: data.media.thumb_link || ("//t.4cdn.org/" + board + "/" + data.media.preview_orig),
  3870.           theight: data.media.preview_h,
  3871.           twidth: data.media.preview_w,
  3872.           isSpoiler: data.media.spoiler === '1'
  3873.         };
  3874.       }
  3875.       $.replace(root.firstChild, Get.cleanPost(Build.post(o, true)));
  3876.       if (cb) {
  3877.         return cb();
  3878.       }
  3879.     },
  3880.     cleanPost: function(root) {
  3881.       var child, el, els, inline, inlined, now, post, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2;
  3882.       post = $('.post', root);
  3883.       _ref = Array.prototype.slice.call(root.childNodes);
  3884.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  3885.         child = _ref[_i];
  3886.         if (child !== post) {
  3887.           $.rm(child);
  3888.         }
  3889.       }
  3890.       _ref1 = $$('.inline', post);
  3891.       for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
  3892.         inline = _ref1[_j];
  3893.         $.rm(inline);
  3894.       }
  3895.       _ref2 = $$('.inlined', post);
  3896.       for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
  3897.         inlined = _ref2[_k];
  3898.         $.rmClass(inlined, 'inlined');
  3899.       }
  3900.       now = Date.now();
  3901.       els = $$('[id]', root);
  3902.       els.push(root);
  3903.       for (_l = 0, _len3 = els.length; _l < _len3; _l++) {
  3904.         el = els[_l];
  3905.         el.id = "" + now + "_" + el.id;
  3906.       }
  3907.       $.rmClass(root, 'forwarded');
  3908.       $.rmClass(root, 'qphl');
  3909.       $.rmClass(post, 'highlight');
  3910.       $.rmClass(post, 'qphl');
  3911.       root.hidden = post.hidden = false;
  3912.       return root;
  3913.     },
  3914.     title: function(thread) {
  3915.       var el, op, span;
  3916.       op = $('.op', thread);
  3917.       el = $('.postInfo .subject', op);
  3918.       if (!el.textContent) {
  3919.         el = $('blockquote', op);
  3920.         if (!el.textContent) {
  3921.           el = $('.nameBlock', op);
  3922.         }
  3923.       }
  3924.       span = $.el('span', {
  3925.         innerHTML: el.innerHTML.replace(/<br>/g, ' ')
  3926.       });
  3927.       return "/" + g.BOARD + "/ - " + (span.textContent.trim());
  3928.     }
  3929.   };
  3930.  
  3931.   Build = {
  3932.     spoilerRange: {},
  3933.     shortFilename: function(filename, isOP) {
  3934.       var threshold;
  3935.       threshold = isOP ? 40 : 30;
  3936.       if (filename.length - 4 > threshold) {
  3937.         return "" + filename.slice(0, threshold - 5) + "(...)." + filename.slice(-3);
  3938.       } else {
  3939.         return filename;
  3940.       }
  3941.     },
  3942.     postFromObject: function(data, board) {
  3943.       var o;
  3944.       o = {
  3945.         postID: data.no,
  3946.         threadID: data.resto || data.no,
  3947.         board: board,
  3948.         name: data.name,
  3949.         capcode: data.capcode,
  3950.         tripcode: data.trip,
  3951.         uniqueID: data.id,
  3952.         email: data.email ? encodeURI(data.email.replace(/&quot;/g, '"')) : '',
  3953.         subject: data.sub,
  3954.         flagCode: data.country,
  3955.         flagName: data.country_name,
  3956.         date: data.now,
  3957.         dateUTC: data.time,
  3958.         comment: data.com,
  3959.         isSticky: !!data.sticky,
  3960.         isClosed: !!data.closed
  3961.       };
  3962.       if (data.ext || data.filedeleted) {
  3963.         o.file = {
  3964.           name: data.filename + data.ext,
  3965.           timestamp: "" + data.tim + data.ext,
  3966.           url: "//i.4cdn.org/" + board + "/" + data.tim + data.ext,
  3967.           height: data.h,
  3968.           width: data.w,
  3969.           MD5: data.md5,
  3970.           size: data.fsize,
  3971.           turl: "//t.4cdn.org/" + board + "/" + data.tim + "s.jpg",
  3972.           theight: data.tn_h,
  3973.           twidth: data.tn_w,
  3974.           isSpoiler: !!data.spoiler,
  3975.           isDeleted: !!data.filedeleted
  3976.         };
  3977.       }
  3978.       return Build.post(o);
  3979.     },
  3980.     post: function(o, isArchived) {
  3981.       /*
  3982. This function contains code from 4chan-JS (https://github.com/4chan/4chan-JS).
  3983. @license: https://github.com/4chan/4chan-JS/blob/master/LICENSE
  3984. */
  3985.  
  3986.       var a, board, capcode, capcodeClass, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref;
  3987.       postID = o.postID, threadID = o.threadID, board = o.board, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, file = o.file;
  3988.       isOP = postID === threadID;
  3989.       staticPath = '//s.4cdn.org';
  3990.       if (email) {
  3991.         emailStart = '<a href="mailto:' + email + '" class="useremail">';
  3992.         emailEnd = '</a>';
  3993.       } else {
  3994.         emailStart = '';
  3995.         emailEnd = '';
  3996.       }
  3997.       subject = "<span class=subject>" + (subject || '') + "</span>";
  3998.       userID = !capcode && uniqueID ? (" <span class='posteruid id_" + uniqueID + "'>(ID: ") + ("<span class=hand title='Highlight posts by this ID'>" + uniqueID + "</span>)</span> ") : '';
  3999.       switch (capcode) {
  4000.         case 'admin':
  4001.         case 'admin_highlight':
  4002.           capcodeClass = " capcodeAdmin";
  4003.           capcodeStart = " <strong class='capcode hand id_admin'" + "title='Highlight posts by the Administrator'>## Admin</strong>";
  4004.           capcode = (" <img src='" + staticPath + "/image/adminicon.gif' ") + "alt='This user is the 4chan Administrator.' " + "title='This user is the 4chan Administrator.' class=identityIcon>";
  4005.           break;
  4006.         case 'mod':
  4007.           capcodeClass = " capcodeMod";
  4008.           capcodeStart = " <strong class='capcode hand id_mod' " + "title='Highlight posts by Moderators'>## Mod</strong>";
  4009.           capcode = (" <img src='" + staticPath + "/image/modicon.gif' ") + "alt='This user is a 4chan Moderator.' " + "title='This user is a 4chan Moderator.' class=identityIcon>";
  4010.           break;
  4011.         case 'developer':
  4012.           capcodeClass = " capcodeDeveloper";
  4013.           capcodeStart = " <strong class='capcode hand id_developer' " + "title='Highlight posts by Developers'>## Developer</strong>";
  4014.           capcode = (" <img src='" + staticPath + "/image/developericon.gif' ") + "alt='This user is a 4chan Developer.' " + "title='This user is a 4chan Developer.' class=identityIcon>";
  4015.           break;
  4016.         default:
  4017.           capcodeClass = '';
  4018.           capcodeStart = '';
  4019.           capcode = '';
  4020.       }
  4021.       flag = flagCode ? (" <img src='" + staticPath + "/image/country/" + (board === 'pol' ? 'troll/' : '')) + flagCode.toLowerCase() + (".gif' alt=" + flagCode + " title='" + flagName + "' class=countryFlag>") : '';
  4022.       if (file != null ? file.isDeleted : void 0) {
  4023.         fileHTML = isOP ? ("<div class=file id=f" + postID + "><div class=fileInfo></div><span class=fileThumb>") + ("<img src='" + staticPath + "/image/filedeleted.gif' alt='File deleted.' class='fileDeleted retina'>") + "</span></div>" : ("<div id=f" + postID + " class=file><span class=fileThumb>") + ("<img src='" + staticPath + "/image/filedeleted-res.gif' alt='File deleted.' class='fileDeletedRes retina'>") + "</span></div>";
  4024.       } else if (file) {
  4025.         ext = file.name.slice(-3);
  4026.         if (!file.twidth && !file.theight && ext === 'gif') {
  4027.           file.twidth = file.width;
  4028.           file.theight = file.height;
  4029.         }
  4030.         fileSize = $.bytesToString(file.size);
  4031.         fileThumb = file.turl;
  4032.         if (file.isSpoiler) {
  4033.           fileSize = "Spoiler Image, " + fileSize;
  4034.           if (!isArchived) {
  4035.             fileThumb = '//s.4cdn.org/image/spoiler';
  4036.             if (spoilerRange = Build.spoilerRange[board]) {
  4037.               fileThumb += ("-" + board) + Math.floor(1 + spoilerRange * Math.random());
  4038.             }
  4039.             fileThumb += '.png';
  4040.             file.twidth = file.theight = 100;
  4041.           }
  4042.         }
  4043.         imgSrc = ("<a class='fileThumb" + (file.isSpoiler ? ' imgspoiler' : '') + "' href='" + file.url + "' target=_blank>") + ("<img src='" + fileThumb + "' alt='" + fileSize + "' data-md5=" + file.MD5 + " style='width:" + file.twidth + "px;height:" + file.theight + "px'></a>");
  4044.         a = $.el('a', {
  4045.           innerHTML: file.name
  4046.         });
  4047.         filename = a.textContent.replace(/%22/g, '"');
  4048.         a.textContent = Build.shortFilename(filename);
  4049.         shortFilename = a.innerHTML;
  4050.         a.textContent = filename;
  4051.         filename = a.innerHTML.replace(/'/g, '&apos;');
  4052.         fileDims = ext === 'pdf' ? 'PDF' : "" + file.width + "x" + file.height;
  4053.         fileInfo = ("<div class=fileText id=fT" + postID + (file.isSpoiler ? " title='" + filename + "'" : '') + ">File: <a" + ((filename !== shortFilename && !file.isSpoiler) ? " title='" + filename + "'" : '') + " href='" + file.url + "' target=_blank>" + (file.isSpoiler ? 'Spoiler Image' : shortFilename) + "</a>") + ("-(" + fileSize + ", " + fileDims) + ")</div>";
  4054.         fileHTML = "<div class=file id=f" + postID + ">" + fileInfo + imgSrc + "</div>";
  4055.       } else {
  4056.         fileHTML = '';
  4057.       }
  4058.       tripcode = tripcode ? " <span class=postertrip>" + tripcode + "</span>" : '';
  4059.       sticky = isSticky ? ' <img src=//s.4cdn.org/image/sticky.gif alt=Sticky title=Sticky style="height:16px;width:16px">' : '';
  4060.       closed = isClosed ? ' <img src=//s.4cdn.org/image/closed.gif alt=Closed title=Closed style="height:16px;width:16px">' : '';
  4061.       container = $.el('div', {
  4062.         id: "pc" + postID,
  4063.         className: "postContainer " + (isOP ? 'op' : 'reply') + "Container",
  4064.         innerHTML: (isOP ? '' : "<div class=sideArrows id=sa" + postID + ">&gt;&gt;</div>") + ("<div id=p" + postID + " class='post " + (isOP ? 'op' : 'reply') + (capcode === 'admin_highlight' ? ' highlightPost' : '') + "'>") + ("<div class='postInfoM mobile' id=pim" + postID + ">") + ("<span class='nameBlock" + capcodeClass + "'>") + ("<span class=name>" + (name || '') + "</span>") + tripcode + capcodeStart + capcode + userID + flag + sticky + closed + ("<br>" + subject) + ("</span><span class='dateTime postNum' data-utc=" + dateUTC + ">" + date) + '<br><em>' + ("<a href=" + ("/" + board + "/thread/" + threadID + "#p" + postID) + ">No.</a>") + ("<a href='" + (g.REPLY && g.THREAD_ID === threadID ? "javascript:quote(" + postID + ")" : "/" + board + "/thread/" + threadID + "#q" + postID) + "'>" + postID + "</a>") + '</em></span>' + '</div>' + (isOP ? fileHTML : '') + ("<div class='postInfo desktop' id=pi" + postID + ">") + ("<input type=checkbox name=" + postID + " value=delete> ") + ("" + subject + " ") + ("<span class='nameBlock" + capcodeClass + "'>") + emailStart + ("<span class=name>" + (name || '') + "</span>") + tripcode + capcodeStart + emailEnd + capcode + userID + flag + sticky + closed + ' </span> ' + ("<span class=dateTime data-utc=" + dateUTC + ">" + date + "</span> ") + "<span class='postNum desktop'>" + ("<a href=" + ("/" + board + "/thread/" + threadID + "#p" + postID) + " title='Highlight this post'>No.</a>") + ("<a href='" + (g.REPLY && +g.THREAD_ID === threadID ? "javascript:quote(" + postID + ")" : "/" + board + "/thread/" + threadID + "#q" + postID) + "' title='Quote this post'>" + postID + "</a>") + '</span>' + '</div>' + (isOP ? '' : fileHTML) + ("<blockquote class=postMessage id=m" + postID + ">" + (comment || '') + "</blockquote> ") + '</div>'
  4065.       });
  4066.       _ref = $$('.quotelink', container);
  4067.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4068.         quote = _ref[_i];
  4069.         href = quote.getAttribute('href');
  4070.         if (href[0] === '/') {
  4071.           continue;
  4072.         }
  4073.         quote.href = "/" + board + "/thread/" + threadID + href;
  4074.       }
  4075.       return container;
  4076.     }
  4077.   };
  4078.  
  4079.   TitlePost = {
  4080.     init: function() {
  4081.       return d.title = Get.title();
  4082.     }
  4083.   };
  4084.  
  4085.   QuoteBacklink = {
  4086.     init: function() {
  4087.       var format;
  4088.       format = Conf['backlink'].replace(/%id/g, "' + id + '");
  4089.       this.funk = Function('id', "return '" + format + "'");
  4090.       return Main.callbacks.push(this.node);
  4091.     },
  4092.     node: function(post) {
  4093.       var a, container, el, link, qid, quote, quotes, _i, _len, _ref;
  4094.       if (post.isInlined) {
  4095.         return;
  4096.       }
  4097.       quotes = {};
  4098.       _ref = post.quotes;
  4099.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4100.         quote = _ref[_i];
  4101.         if (quote.parentNode.parentNode.className === 'capcodeReplies') {
  4102.           break;
  4103.         }
  4104.         if (quote.hostname === 'boards.4chan.org' && !/catalog$/.test(quote.pathname) && (qid = quote.hash.slice(2))) {
  4105.           quotes[qid] = true;
  4106.         }
  4107.       }
  4108.       a = $.el('a', {
  4109.         href: "/" + g.BOARD + "/thread/" + post.threadID + "#p" + post.ID,
  4110.         className: post.el.hidden ? 'filtered backlink' : 'backlink',
  4111.         textContent: QuoteBacklink.funk(post.ID)
  4112.       });
  4113.       for (qid in quotes) {
  4114.         if (!(el = $.id("pi" + qid)) || !Conf['OP Backlinks'] && /\bop\b/.test(el.parentNode.className)) {
  4115.           continue;
  4116.         }
  4117.         link = a.cloneNode(true);
  4118.         if (Conf['Quote Preview']) {
  4119.           $.on(link, 'mouseover', QuotePreview.mouseover);
  4120.         }
  4121.         if (Conf['Quote Inline']) {
  4122.           $.on(link, 'click', QuoteInline.toggle);
  4123.         }
  4124.         if (!(container = $.id("blc" + qid))) {
  4125.           container = $.el('span', {
  4126.             className: 'container',
  4127.             id: "blc" + qid
  4128.           });
  4129.           $.add(el, container);
  4130.         }
  4131.         $.add(container, [$.tn(' '), link]);
  4132.       }
  4133.     }
  4134.   };
  4135.  
  4136.   QuoteInline = {
  4137.     init: function() {
  4138.       return Main.callbacks.push(this.node);
  4139.     },
  4140.     node: function(post) {
  4141.       var quote, _i, _j, _len, _len1, _ref, _ref1;
  4142.       _ref = post.quotes;
  4143.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4144.         quote = _ref[_i];
  4145.         if (!(quote.hash && quote.hostname === 'boards.4chan.org' && !/catalog$/.test(quote.pathname) || /\bdeadlink\b/.test(quote.className))) {
  4146.           continue;
  4147.         }
  4148.         $.on(quote, 'click', QuoteInline.toggle);
  4149.       }
  4150.       _ref1 = post.backlinks;
  4151.       for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
  4152.         quote = _ref1[_j];
  4153.         $.on(quote, 'click', QuoteInline.toggle);
  4154.       }
  4155.     },
  4156.     toggle: function(e) {
  4157.       var id;
  4158.       if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
  4159.         return;
  4160.       }
  4161.       e.preventDefault();
  4162.       id = this.dataset.id || this.hash.slice(2);
  4163.       if (/\binlined\b/.test(this.className)) {
  4164.         QuoteInline.rm(this, id);
  4165.       } else {
  4166.         if ($.x("ancestor::div[contains(@id,'p" + id + "')]", this)) {
  4167.           return;
  4168.         }
  4169.         QuoteInline.add(this, id);
  4170.       }
  4171.       return this.classList.toggle('inlined');
  4172.     },
  4173.     add: function(q, id) {
  4174.       var board, el, i, inline, isBacklink, path, postID, root, threadID;
  4175.       if (q.host === 'boards.4chan.org') {
  4176.         path = q.pathname.split('/');
  4177.         board = path[1];
  4178.         threadID = path[3];
  4179.         postID = id;
  4180.       } else {
  4181.         board = q.dataset.board;
  4182.         threadID = 0;
  4183.         postID = q.dataset.id;
  4184.       }
  4185.       el = board === g.BOARD ? $.id("p" + postID) : false;
  4186.       inline = $.el('div', {
  4187.         id: "i" + postID,
  4188.         className: el ? 'inline' : 'inline crosspost'
  4189.       });
  4190.       root = (isBacklink = /\bbacklink\b/.test(q.className)) ? q.parentNode : $.x('ancestor-or-self::*[parent::blockquote][1]', q);
  4191.       $.after(root, inline);
  4192.       Get.post(board, threadID, postID, inline);
  4193.       if (!el) {
  4194.         return;
  4195.       }
  4196.       if (isBacklink && Conf['Forward Hiding']) {
  4197.         $.addClass(el.parentNode, 'forwarded');
  4198.         ++el.dataset.forwarded || (el.dataset.forwarded = 1);
  4199.       }
  4200.       if ((i = Unread.replies.indexOf(el)) !== -1) {
  4201.         Unread.replies.splice(i, 1);
  4202.         return Unread.update(true);
  4203.       }
  4204.     },
  4205.     rm: function(q, id) {
  4206.       var div, inlined, _i, _len, _ref;
  4207.       div = $.x("following::div[@id='i" + id + "']", q);
  4208.       $.rm(div);
  4209.       if (!Conf['Forward Hiding']) {
  4210.         return;
  4211.       }
  4212.       _ref = $$('.backlink.inlined', div);
  4213.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4214.         inlined = _ref[_i];
  4215.         div = $.id(inlined.hash.slice(1));
  4216.         if (!--div.dataset.forwarded) {
  4217.           $.rmClass(div.parentNode, 'forwarded');
  4218.         }
  4219.       }
  4220.       if (/\bbacklink\b/.test(q.className)) {
  4221.         div = $.id("p" + id);
  4222.         if (!--div.dataset.forwarded) {
  4223.           return $.rmClass(div.parentNode, 'forwarded');
  4224.         }
  4225.       }
  4226.     }
  4227.   };
  4228.  
  4229.   QuotePreview = {
  4230.     init: function() {
  4231.       return Main.callbacks.push(this.node);
  4232.     },
  4233.     node: function(post) {
  4234.       var quote, _i, _j, _len, _len1, _ref, _ref1;
  4235.       _ref = post.quotes;
  4236.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4237.         quote = _ref[_i];
  4238.         if (!(quote.hash && quote.hostname === 'boards.4chan.org' && !/catalog$/.test(quote.pathname) || /\bdeadlink\b/.test(quote.className))) {
  4239.           continue;
  4240.         }
  4241.         $.on(quote, 'mouseover', QuotePreview.mouseover);
  4242.       }
  4243.       _ref1 = post.backlinks;
  4244.       for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
  4245.         quote = _ref1[_j];
  4246.         $.on(quote, 'mouseover', QuotePreview.mouseover);
  4247.       }
  4248.     },
  4249.     mouseover: function(e) {
  4250.       var board, el, path, postID, qp, quote, quoterID, threadID, _i, _len, _ref;
  4251.       if (/\binlined\b/.test(this.className)) {
  4252.         return;
  4253.       }
  4254.       if (qp = $.id('qp')) {
  4255.         if (qp === UI.el) {
  4256.           delete UI.el;
  4257.         }
  4258.         $.rm(qp);
  4259.       }
  4260.       if (UI.el) {
  4261.         return;
  4262.       }
  4263.       if (this.host === 'boards.4chan.org') {
  4264.         path = this.pathname.split('/');
  4265.         board = path[1];
  4266.         threadID = path[3];
  4267.         postID = this.hash.slice(2);
  4268.       } else {
  4269.         board = this.dataset.board;
  4270.         threadID = 0;
  4271.         postID = this.dataset.id;
  4272.       }
  4273.       qp = UI.el = $.el('div', {
  4274.         id: 'qp',
  4275.         className: 'reply dialog'
  4276.       });
  4277.       UI.hover(e);
  4278.       $.add(d.body, qp);
  4279.       if (board === g.BOARD) {
  4280.         el = $.id("p" + postID);
  4281.       }
  4282.       Get.post(board, threadID, postID, qp, function() {
  4283.         var bq, img, post;
  4284.         bq = $('blockquote', qp);
  4285.         Main.prettify(bq);
  4286.         post = {
  4287.           el: qp,
  4288.           blockquote: bq,
  4289.           isArchived: /\barchivedPost\b/.test(qp.className)
  4290.         };
  4291.         if (img = $('img[data-md5]', qp)) {
  4292.           post.fileInfo = img.parentNode.previousElementSibling;
  4293.           post.img = img;
  4294.         }
  4295.         if (Conf['Reveal Spoilers']) {
  4296.           RevealSpoilers.node(post);
  4297.         }
  4298.         if (Conf['Image Auto-Gif']) {
  4299.           AutoGif.node(post);
  4300.         }
  4301.         if (Conf['Time Formatting']) {
  4302.           Time.node(post);
  4303.         }
  4304.         if (Conf['File Info Formatting']) {
  4305.           FileInfo.node(post);
  4306.         }
  4307.         if (Conf['Resurrect Quotes']) {
  4308.           Quotify.node(post);
  4309.         }
  4310.         if (Conf['Anonymize']) {
  4311.           return Anonymize.node(post);
  4312.         }
  4313.       });
  4314.       $.on(this, 'mousemove', UI.hover);
  4315.       $.on(this, 'mouseout click', QuotePreview.mouseout);
  4316.       if (!el) {
  4317.         return;
  4318.       }
  4319.       if (Conf['Quote Highlighting']) {
  4320.         if (/\bop\b/.test(el.className)) {
  4321.           $.addClass(el.parentNode, 'qphl');
  4322.         } else {
  4323.           $.addClass(el, 'qphl');
  4324.         }
  4325.       }
  4326.       quoterID = $.x('ancestor::*[@id][1]', this).id.match(/\d+$/)[0];
  4327.       _ref = $$('.quotelink, .backlink', qp);
  4328.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4329.         quote = _ref[_i];
  4330.         if (quote.hash.slice(2) === quoterID) {
  4331.           $.addClass(quote, 'forwardlink');
  4332.         }
  4333.       }
  4334.     },
  4335.     mouseout: function(e) {
  4336.       var el;
  4337.       UI.hoverend();
  4338.       if (el = $.id(this.hash.slice(1))) {
  4339.         $.rmClass(el, 'qphl');
  4340.         $.rmClass(el.parentNode, 'qphl');
  4341.       }
  4342.       $.off(this, 'mousemove', UI.hover);
  4343.       return $.off(this, 'mouseout click', QuotePreview.mouseout);
  4344.     }
  4345.   };
  4346.  
  4347.   QuoteOP = {
  4348.     init: function() {
  4349.       return Main.callbacks.push(this.node);
  4350.     },
  4351.     node: function(post) {
  4352.       var quote, _i, _len, _ref;
  4353.       if (post.isInlined && !post.isCrosspost) {
  4354.         return;
  4355.       }
  4356.       _ref = post.quotes;
  4357.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4358.         quote = _ref[_i];
  4359.         if (quote.hash.slice(2) === post.threadID) {
  4360.           $.add(quote, $.tn('\u00A0(OP)'));
  4361.         }
  4362.       }
  4363.     }
  4364.   };
  4365.  
  4366.   QuoteCT = {
  4367.     init: function() {
  4368.       return Main.callbacks.push(this.node);
  4369.     },
  4370.     node: function(post) {
  4371.       var path, quote, _i, _len, _ref;
  4372.       if (post.isInlined && !post.isCrosspost) {
  4373.         return;
  4374.       }
  4375.       _ref = post.quotes;
  4376.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4377.         quote = _ref[_i];
  4378.         if (!(quote.hash && quote.hostname === 'boards.4chan.org' && !/catalog$/.test(quote.pathname))) {
  4379.           continue;
  4380.         }
  4381.         path = quote.pathname.split('/');
  4382.         if (path[1] === g.BOARD && path[3] !== post.threadID) {
  4383.           $.add(quote, $.tn('\u00A0(Cross-thread)'));
  4384.         }
  4385.       }
  4386.     }
  4387.   };
  4388.  
  4389.   Quotify = {
  4390.     init: function() {
  4391.       return Main.callbacks.push(this.node);
  4392.     },
  4393.     node: function(post) {
  4394.       var a, board, deadlink, id, m, postBoard, quote, _i, _len, _ref, _ref1;
  4395.       if (post.isInlined && !post.isCrosspost) {
  4396.         return;
  4397.       }
  4398.       _ref = $$('.deadlink', post.blockquote);
  4399.       for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  4400.         deadlink = _ref[_i];
  4401.         if (deadlink.parentNode.className === 'prettyprint') {
  4402.           $.replace(deadlink, Array.prototype.slice.call(deadlink.childNodes));
  4403.           continue;
  4404.         }
  4405.         quote = deadlink.textContent;
  4406.         a = $.el('a', {
  4407.           textContent: "" + quote + "\u00A0(Dead)"
  4408.         });
  4409.         if (!(id = (_ref1 = quote.match(/\d+$/)) != null ? _ref1[0] : void 0)) {
  4410.           continue;
  4411.         }
  4412.         if (m = quote.match(/^>>>\/([a-z\d]+)/)) {
  4413.           board = m[1];
  4414.         } else if (postBoard) {
  4415.           board = postBoard;
  4416.         } else {
  4417.           board = postBoard = $('a[title="Highlight this post"]', post.el).pathname.split('/')[1];
  4418.         }
  4419.         if (board === g.BOARD && $.id("p" + id)) {
  4420.           a.href = "#p" + id;
  4421.           a.className = 'quotelink';
  4422.         } else {
  4423.           a.href = Redirect.to({
  4424.             board: board,
  4425.             threadID: 0,
  4426.             postID: id
  4427.           });
  4428.           a.className = 'deadlink';
  4429.           a.target = '_blank';
  4430.           if (Redirect.post(board, id)) {
  4431.             $.addClass(a, 'quotelink');
  4432.             a.setAttribute('data-board', board);
  4433.             a.setAttribute('data-id', id);
  4434.           }
  4435.         }
  4436.         $.replace(deadlink, a);
  4437.       }
  4438.     }
  4439.   };
  4440.  
  4441.   DeleteLink = {
  4442.     init: function() {
  4443.       var aImage, aPost, children, div;
  4444.       div = $.el('div', {
  4445.         className: 'delete_link',
  4446.         textContent: 'Delete'
  4447.       });
  4448.       aPost = $.el('a', {
  4449.         className: 'delete_post',
  4450.         href: 'javascript:;'
  4451.       });
  4452.       aImage = $.el('a', {
  4453.         className: 'delete_image',
  4454.         href: 'javascript:;'
  4455.       });
  4456.       children = [];
  4457.       children.push({
  4458.         el: aPost,
  4459.         open: function() {
  4460.           aPost.textContent = 'Post';
  4461.           $.on(aPost, 'click', DeleteLink["delete"]);
  4462.           return true;
  4463.         }
  4464.       });
  4465.       children.push({
  4466.         el: aImage,
  4467.         open: function(post) {
  4468.           if (!post.img) {
  4469.             return false;
  4470.           }
  4471.           aImage.textContent = 'Image';
  4472.           $.on(aImage, 'click', DeleteLink["delete"]);
  4473.           return true;
  4474.         }
  4475.       });
  4476.       Menu.addEntry({
  4477.         el: div,
  4478.         open: function(post) {
  4479.           var node, seconds;
  4480.           if