您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Brings together comments and their replies.
- // ==UserScript==
- // @name FR Tree Viewer
- // @namespace http://cynwoody.appspot.com/fr_tree_viewer.html
- // @description Brings together comments and their replies.
- // @date 2015-11-16
- // @include http://*.freerepublic.com*/posts*
- // @include http://freerepublic.com*/posts*
- // @version 0.0.1.20151118234707
- // ==/UserScript==
- const STYLE = '.a2 {clear:both}' +
- '.postBox {' +
- 'padding:3px;' +
- 'border:1px solid #888;' +
- 'margin-top:3px;' +
- 'margin-bottom:3px}' +
- '.quoteButton {color:#009;margin-left:3px;padding:0px}' +
- '.quoteBox {background:#ffc;padding:3px;border:1px solid #ccf}' +
- '#progressBox {position:fixed;left:2px;top:10px;' +
- 'background:#fc0;padding:3px;border:1px solid black}' +
- '#logBox {background:skyblue;padding:3px}' +
- '#scrollBox {position:fixed;top:0px;left:0px;z-index:1;' +
- 'border:1px solid #888;background:yellow}' +
- '#scrollButtons {padding:2px}' +
- '#scrollButtons input {padding:0px;margin:0px;color:blue}' +
- '#scrollClose, #scrollClose input {' +
- 'margin:0px;padding:0px;text-align:right;' +
- 'color:red;font-size:7px;font-weight:bold}';
- const INDENT = 7; // Indentation per reply level, in pixels
- const MAX_INDENT = 0.7; // Maximum indent, as a fraction of window width
- const BACKGROUNDS = ['#ccf', '#ffc', '#cfc', '#fcc', '#cff', '#fcf', '#ccc'];
- const SCROLL = {
- method: "constantTime", // Change to "jump" to eliminate animation
- constantSpeed: {
- pixelsPerInterval: 50,
- timeInterval: 10
- },
- constantTime: {
- intervalCount: 25,
- timeInterval: 20
- }
- };
- var posts; // Maps postNumber to post (as HTML text)
- var replies; // Maps postNumber to a list of replying post numbers
- var postCheck; // Cross-check. Used to pick up deleted posts
- var maxIndent; // Maximum indent, in pixels
- var indexingCanceled; // Set when user cancels indexing of posts
- var selection; // Text selected when Quote button pressed
- // Keystroke watcher. Implements keyboard shortcuts:
- // Ctrl-Alt-T => Tree View
- // Ctrl-Alt-R => Poster Report
- function onKeyPress(event)
- {
- if (event.ctrlKey && event.altKey) {
- switch (event.charCode) {
- case 116: // t in Firefox
- case 20: // t in Chrome
- onTreeViewClick(event, $('treeButton'));
- break;
- case 114: // r in Firefox
- case 18: // r in Chrome
- onPosterReportClick(event, $('posterReport'));
- break;
- }
- }
- }
- // Handles Tree View button click (or Ctrl-Alt-T).
- function onTreeViewClick(event, button)
- {
- button = button || this;
- if (button.disabled)
- return;
- var callback = /Tree/.test(button.value) ? makeTreeView : makeFlatView;
- disable(button, 'Waiting ...');
- disable('posterReport');
- indexThread(button, callback);
- }
- // Rearranges the posted comments into the tree view, assuming the
- // thread has been indexed successfully (ok == true).
- function makeTreeView(ok)
- {
- if (!ok) {
- enable('treeButton', 'View as Tree');
- enable('posterReport');
- return;
- }
- var start = new Date();
- var div = deleteExistingPosts();
- maxIndent = window.innerWidth * MAX_INDENT;
- postCheck = {count:0};
- addReply(div, 1, 0);
- if (postCheck.count < posts.length-1)
- addDeletedPosts(div);
- if (postCheck.count != posts.length-1)
- alert('postCheck = ' + postCheck.count + ', but there are ' +
- (posts.length-1) + ' posts.');
- doFixups();
- addScrollBox();
- enable('treeButton', 'Flat View');
- enable('posterReport');
- logTime(start, 'generate the tree view');
- }
- makeTreeView.title = 'tree view';
- // Makes a second pass over the posts to pick up deleted posts and
- // their replies. These would otherwise be missed, because deleted
- // posts have no To link.
- function addDeletedPosts(container)
- {
- for (var x=1, limit=posts.length; x<limit; ++x) {
- if (!postCheck[x])
- addReply(container, x, 0);
- }
- }
- // Adds a post to the display with the indentation indicated by
- // depth. Then calls itself to add each reply at the next deeper
- // indentation level.
- function addReply(container, postNumber, depth)
- {
- ++postCheck.count;
- postCheck[postNumber] = true;
- var div = document.createElement('div');
- div.className = 'postBox';
- var indent = depth * INDENT;
- if (indent > maxIndent)
- indent = maxIndent;
- div.style.marginLeft = indent + 'px';
- div.style.background = BACKGROUNDS[depth % BACKGROUNDS.length];
- div.innerHTML = posts[postNumber];
- var anchor = document.createElement('a');
- anchor.name = postNumber;
- container.appendChild(anchor);
- container.appendChild(div);
- var replyList = replies[postNumber];
- if (replyList) {
- ++depth;
- for (var x=0, limit=replyList.length; x<limit; ++x)
- addReply(container, replyList[x], depth);
- }
- }
- // Rearranges the posts into the chronological flat view, similar to
- // FR's normal view, but always showing all the posts.
- function makeFlatView(ok)
- {
- if (!ok) {
- enable('treeButton', 'View as Tree');
- enable('posterReport');
- return;
- }
- var start = new Date();
- var div = deleteExistingPosts();
- var s = '';
- for (var postNumber=1, limit=posts.length; postNumber<limit; ++postNumber) {
- s += '<a name=' + postNumber + '></a>\n';
- s += posts[postNumber];
- s += '<hr size=1 noshade=noshade>\n';
- }
- div.innerHTML = s;
- removeScrollBox();
- doFixups();
- enable('treeButton', 'Tree View');
- enable('posterReport');
- logTime(start, 'generate the flat view');
- }
- makeTreeView.title = 'flat view';
- // Performs fixups needed after view creation (tree or flat).
- function doFixups()
- {
- fixLinks(document.body);
- removeBlankLines(document.body);
- addQuoteButtons(document.body);
- localizeDates(document.body);
- decrudify();
- }
- // Rewrites all the To-links in the node in such a was as to ensure
- // they are valid in the new, single-page environment.
- function fixLinks(node)
- {
- var links = $x('.//a[contains(., "To ")]', node);
- for (var x=0, limit=links.snapshotLength; x<limit; ++x) {
- var link = links.snapshotItem(x);
- link.href = link.hash;
- }
- }
- // Removes the existing posts and replaces them with an empty div
- // ready to receive the rearranged posts. Returns the empty div.
- function deleteExistingPosts()
- {
- var firstNode = findBeginningOfPosts();
- var lastNode = document.body.lastChild;
- do {
- lastNode = lastNode.previousSibling;
- } while (!/Disclaimer/.test(lastNode.innerHTML));
- var range = document.createRange();
- range.setStartAfter(firstNode);
- range.setEndBefore(lastNode);
- range.deleteContents();
- range.detach();
- var div = document.createElement('div');
- div.id = 'posts';
- document.body.insertBefore(div, lastNode);
- return div;
- }
- // Locates the node before the first post. If the user is signed in,
- // we can use the 'comment' anchor. Otherwise, we have to find the
- // actual first post and back up to the horizontal rule.
- function findBeginningOfPosts()
- {
- var firstNode = document.anchors.namedItem('comment');
- if (firstNode)
- return firstNode;
- var node = $xFirst('div[@class="b2"]');
- node = node || $xFirst('div[@id="posts"]');
- while (node) {
- node = node.previousSibling;
- if (node.tagName == 'HR')
- return node;
- }
- alert("Can't find posts!");
- return null;
- }
- // Parses all the posts in the thread and builds two tables (unless
- // they already exist):
- // - posts, an array that maps a postNumber to the post's HTML
- // snippet.
- // - replies, a hash that links a postNumber to an array of
- // replying postNumbers.
- // Calls the callback function when the indexing is complete (it will
- // happen asynchronously if page fetches are required).
- function indexThread(button, callback)
- {
- if (replies)
- callback(true);
- else {
- var first = $xFirst('a[@class="fr_page_goto"][contains(., "first")]');
- if (first) {
- button.value = 'Waiting ...';
- indexWholeThread(first, callback);
- }
- else {
- var start = new Date();
- indexPosts(originalHTML);
- indexReplies();
- var now = new Date();
- logTime(start, 'index the thread');
- callback(true);
- }
- }
- }
- // Called by indexThread to index multipage threads. Fetches the entire
- // thread in the background, adding the posts of each page to the posts
- // index. Then indexes the replies and calls the callback function,
- // indicating whether the operation completed or was canceled by the
- // user.
- function indexWholeThread(firstLink, callback)
- {
- var start = new Date();
- var link = document.createElement('a');
- link.href = firstLink.href;
- link.hash = '';
- loadLink(link, loadNext);
- function loadNext(html)
- {
- if (indexingCanceled) {
- indexingCanceled = posts = replies = null;
- hideProgress();
- log('Loading and indexing canceled!');
- callback(false);
- return;
- }
- if (!html) {
- callback(false);
- return;
- }
- var r = html.match(/href="posts(\?[^"]*)" class="fr_page_goto"[^>]*>next</);
- if (r) {
- link.search = r[1];
- loadLink(link, loadNext);
- }
- else {
- showProgress('Generating ' + callback.title + ' ...');
- var postCount = posts.length - 1;
- indexReplies();
- var now = new Date();
- logTime(start, 'load and index');
- callback(true);
- hideProgress();
- }
- }
- }
- // Displays a message in floating box to let the user know how far a
- // multipage indexing operation has proceeded. The box includes a Cancel
- // button, in case the user decides to bail.
- function showProgress(msg)
- {
- var progress = $('progressMsg');
- if (!progress)
- progress = makeProgressBox();
- progress.innerHTML = msg;
- $('indexingCancel').disabled = false;
- $('progressBox').style.display = 'block';
- }
- function makeProgressBox()
- {
- var div = document.createElement('div');
- div.id = 'progressBox';
- div.innerHTML = '<span id=progressMsg></span> ' +
- '<input id=indexingCancel type=button value=Cancel>';
- document.body.appendChild(div);
- $('indexingCancel').addEventListener('click', onProgressCancel, false);
- return $('progressMsg');
- }
- function hideProgress()
- {
- var box = $('progressBox');
- if (box)
- box.style.display = 'none';
- }
- // Handles a progress box cancel click. Signals the indexing to stop.
- function onProgressCancel()
- {
- indexingCanceled = true;
- $('indexingCancel').disabled = true;
- }
- // Fetches a page in the background, indexes it, and calls the callback,
- // passing the retrieved HTML.
- function loadLink(link, callback)
- {
- var req = new XMLHttpRequest();
- req.open('GET', link, true);
- req.onreadystatechange = handler;
- req.send(null);
- showProgress('Loading ' + link);
- function handler()
- {
- if (req.readyState == 4) {
- var html = req.responseText;
- if (req.status != 200) {
- var msg = "XHR received " + req.status + ' ' + req.statusText +
- ' loading ' + link + '.';
- alert(msg);
- log(msg);
- html = null;
- indexingCanceled = true;
- }
- if (html)
- indexPosts(html);
- callback(html);
- }
- }
- }
- // Extracts the posts from a page of HTML text and adds them to the
- // posts array, using postNumber as the subscript.
- function indexPosts(html)
- {
- var r = new RegExp('<a name="(\\d+)"></a>\\n([\\s\\S]+?' +
- '(?:<div class="n2">[\\s\\S]+?</div>|' +
- 'Moderator</i></small><br[ /]*>))', 'gi');
- posts = posts || [];
- var match;
- while (match = r.exec(html))
- posts[match[1]] = match[2];
- }
- // Runs thru the posts array and builds the replies table. The replies
- // table contains an array of replying post numbers for each post that
- // has at least one reply.
- function indexReplies()
- {
- replies = {};
- for (var postNumber=1, limit=posts.length; postNumber<limit; ++postNumber) {
- var m = /<a .*?href=".*?#(\d+)">To \1</i.exec(posts[postNumber]);;
- if (m && m[1]) {
- var toNumber = m[1];
- var replyList = replies[toNumber];
- if (replyList)
- replyList.push(postNumber);
- else
- replies[toNumber] = [postNumber];
- }
- }
- }
- // Adds a draggable floating box which appears when the user clicks in
- // in the white indentation space of the tree view. The box includes
- // buttons to scroll up or down to the next post at or above the box's
- // indent level.
- function addScrollBox()
- {
- var div = document.createElement('div');
- div.id = 'scrollBox';
- div.style.display = 'none';
- div.innerHTML = '<div id=scrollClose>' +
- '<input id=scrollCloseButton type=button value=x></div>' +
- '<div id=scrollButtons>' +
- '<input id=up type=button value=Up><br>' +
- '<input id=dn type=button value=Dn>' +
- '</div>';
- div.title = 'Scrolls up or down to next comment of same or outer color. ' +
- 'Drag to change color.';
- document.body.appendChild(div);
- $('posts').addEventListener('click', onPostsClick, false);
- $('scrollCloseButton').addEventListener('click', onScrollClose, false);
- var list = $x('.//input', div);
- for (var x=0, limit=list.snapshotLength; x<limit; ++x) {
- var button = list.snapshotItem(x);
- button.addEventListener('mousedown', onScrollButtonMouseDown, false);
- }
- div.addEventListener('mousedown', onScrollBoxMouseDown, false);
- $('dn').addEventListener('click', onDnClick, false);
- $('up').addEventListener('click', onUpClick, false);
- }
- // Deletes the scroll box created by addScrollBox, if it exists.
- function removeScrollBox()
- {
- var scrollBox = $('scrollBox');
- if (scrollBox) {
- var posts = scrollBox.parentNode;
- posts.removeChild(scrollBox);
- posts.removeEventListener('click', onPostsClick, false);
- }
- }
- // Responds to a click in the tree view indentation white space by
- // showing the scroll box at the spot clicked.
- function onPostsClick(e)
- {
- if (e.target.id != 'posts')
- return;
- var scrollBox = $('scrollBox');
- scrollBox.style.display = 'block';
- scrollBox.style.left = (e.clientX - scrollBox.offsetWidth/2) + 'px';
- scrollBox.style.top = e.clientY + 'px';
- colorScrollBox(scrollBox);
- }
- // Hides the scroll box when the user clicks its Close button.
- function onScrollClose(e)
- {
- $('scrollBox').style.display = 'none';
- }
- // Traps mousedowns on scroll box buttons, so they won't start a drag.
- function onScrollButtonMouseDown(e)
- {
- e.stopPropagation();
- }
- // Supports dragging the scroll box by its white space areas.
- function onScrollBoxMouseDown(e)
- {
- this.addEventListener('mousemove', onMouseMove, true);
- this.addEventListener('mouseup', onMouseUp, true);
- var mdx = e.clientX;
- var mdy = e.clientY;
- var mdLeft = parseInt(this.style.left);
- var mdTop = parseInt(this.style.top);
- e.stopPropagation();
- function onMouseMove(e)
- {
- var x = e.clientX - mdx;
- var y = e.clientY - mdy;
- this.style.left = mdLeft + x + 'px';
- this.style.top = mdTop + y + 'px';
- colorScrollBox(this);
- e.stopPropagation();
- }
- function onMouseUp(e)
- {
- this.removeEventListener('mousemove', onMouseMove, true);
- this.removeEventListener('mouseup', onMouseUp, true);
- e.stopPropagation();
- }
- }
- // Sets the scroll box's background color to correspond to its current
- // indent level.
- function colorScrollBox(scrollBox)
- {
- var depth = scrollBoxDepth(scrollBox);
- scrollBox.style.background = BACKGROUNDS[depth % BACKGROUNDS.length];
- }
- // Computes the scroll box's indent level, based on its horizontal
- // position.
- function scrollBoxDepth(scrollBox)
- {
- var offset = scrollBox.offsetLeft - $('posts').offsetLeft +
- Math.floor(scrollBox.offsetWidth/2);
- return offset > 0 ? Math.floor(offset/INDENT) : 0;
- }
- // Scrolls up to the next post at or left of the scroll box's indent
- // level.
- function onUpClick(event)
- {
- scrollPosts(event, findUp);
- function findUp(boxDepth, boxTop, divs)
- {
- for (var x=divs.snapshotLength-1; x>=0; --x) {
- var div = divs.snapshotItem(x);
- if (div.offsetTop >= boxTop)
- continue;
- var divDepth = parseInt(div.style.marginLeft) / INDENT;
- if (divDepth <= boxDepth)
- break;
- }
- return div;
- }
- }
- // Scrolls down to the next post at or left of the scroll box's indent
- // level.
- function onDnClick(event)
- {
- scrollPosts(event, findDown);
- function findDown(boxDepth, boxTop, divs)
- {
- for (var x=0, limit=divs.snapshotLength; x<limit; ++x) {
- var div = divs.snapshotItem(x);
- if (div.offsetTop <= boxTop)
- continue;
- var divDepth = parseInt(div.style.marginLeft) / INDENT;
- if (divDepth <= boxDepth)
- break;
- }
- return div;
- }
- }
- // Scrolls the display so that the post found by the findDiv function
- // is opposite the scroll box. Chooses between three different scroll
- // methods, depending on the settings in the SCROLL constant. Available
- // methods include two types of animation and a simple jump. If the
- // control or shift key is down, it always uses the jump method.
- function scrollPosts(event, findDiv)
- {
- var box = $('scrollBox');
- var boxTop = box.offsetTop + window.pageYOffset;
- var boxDepth = scrollBoxDepth(box);
- var divs = $x('div', $('posts'));
- var div = findDiv(boxDepth, boxTop, divs);
- var scrollDistance = div.offsetTop - boxTop;
- if (/jump/i.test(SCROLL.method) || event.ctrlKey || event.shiftKey)
- jump();
- else if (/time/i.test(SCROLL.method))
- constantTime();
- else if (/speed/i.test(SCROLL.method))
- constantSpeed();
- else
- constantTime();
- // Non-animated, simple scroll.
- function jump()
- {
- window.scrollBy(0, scrollDistance);
- }
- var interval;
- // Animated scroll: Moves the display faster or slower depending
- // on the distance to scroll.
- function constantTime()
- {
- var parms = SCROLL.constantTime;
- var intervalCount = parms.intervalCount;
- interval = window.setInterval(scrollABit, parms.timeInterval);
- document.body.addEventListener('click', abort, true);
- function scrollABit()
- {
- if (intervalCount == 0) {
- abort();
- return;
- }
- var scrollInc = Math.round(scrollDistance / intervalCount--);
- window.scrollBy(0, scrollInc);
- scrollDistance -= scrollInc;
- }
- }
- // Animated scroll: Moves the display at a steady speed until the
- // distance is covered.
- function constantSpeed()
- {
- var parms = SCROLL.constantSpeed;
- var scrollInc = parms.pixelsPerInterval;
- if (scrollDistance < 0)
- scrollInc = -scrollInc;
- interval = window.setInterval(scrollABit, parms.timeInterval);
- document.body.addEventListener('click', abort, true);
- function scrollABit()
- {
- if (scrollDistance == 0) {
- abort();
- return;
- }
- if (Math.abs(scrollInc) > Math.abs(scrollDistance))
- scrollInc = scrollDistance;
- window.scrollBy(0, scrollInc);
- scrollDistance -= scrollInc;
- }
- }
- // Terminates an animated scroll early if the user clicks.
- function abort()
- {
- window.clearInterval(interval);
- document.body.removeEventListener('click', arguments.callee, true);
- }
- }
- // -----------------------------------------------------------------------------
- // Handles a mouse press on a Quote button. Installs a click handler for
- // the button, allowing the click to be handled correctly while preserving
- // any text selection the user may have made.
- function onQuotePress()
- {
- this.removeEventListener('click', onQuoteClick, false);
- this.addEventListener('click', onQuoteClick, false);
- var sel = window.getSelection();
- selection = sel.toString();
- sel.removeAllRanges(); // Deselect the text
- }
- // Handles a Quote / Unquote button click. If there is already a quoted
- // post showing (the Unquote button case), we delete it. Otherwise (the
- // Quote button case), we locate the post to be quoted and display it
- // in a box above the current post (the one containing the Quote button).
- // Unless a post number has been selected with the mouse, we quote the
- // the post to which the current post is in reply.
- // If the desired post is not in memory, we will load the page that
- // contains it in the background, while showing the progress bar.
- function onQuoteClick()
- {
- var button = this;
- if (selection) {
- var postNumbers = selection.match(/\d+/g);
- if (postNumbers) {
- hideQuote(button);
- quoteSelectedPostNumbers(button, postNumbers);
- return;
- }
- }
- if (button.value == 'Unquote') {
- hideQuote(button);
- return;
- }
- var postNumber = button.previousSibling.hash.substr(1);
- findAndQuotePost(button, postNumber);
- }
- // Finds and quotes each of a list of posts
- function quoteSelectedPostNumbers(button, postNumbers)
- {
- for (var x=0; x<postNumbers.length; ++x)
- findAndQuotePost(button, postNumbers[x]);
- }
- // Finds and quotes a given post. Runs right away if the desired post
- // is in memory. Otherwise, it loads the page containing the post in
- // the background before continuing.
- function findAndQuotePost(button, postNumber)
- {
- if (findPost(postNumber))
- quotePost(button, postNumber);
- else {
- disable(button, 'Waiting ...');
- var l = window.location;
- var url = l.protocol + '//' + l.hostname + l.pathname +
- '?page=' + postNumber + '#' + postNumber;
- loadLink(url, function() {
- quotePost(button, postNumber);
- });
- button.disabled = false;
- }
- }
- // Looks for the post in the posts index. If there is no posts index,
- // we build one for the current page. Returns undefined if not found.
- function findPost(postNumber)
- {
- hideProgress();
- if (!posts)
- indexPosts(originalHTML);
- return posts[postNumber];
- }
- // Copies the post to be quoted into a box above the post containing
- // the Quote button.
- function quotePost(button, postNumber)
- {
- var post = findPost(postNumber);
- post = post || '<b>Unable to retrieve post #' + postNumber + '.</b>';
- var div = button.ownerDocument.createElement('div');
- div.className = 'quoteBox';
- div.innerHTML = post;
- addQuoteButtons(div);
- removeBlankLines(div);
- fixToLink(div);
- localizeDates(div);
- var anchorDiv = findAnchorDiv(button);
- if (anchorDiv.className != 'postBox')
- anchorDiv.parentNode.insertBefore(div, anchorDiv);
- else
- anchorDiv.insertBefore(div, anchorDiv.firstChild);
- button.value = 'Unquote';
- window.scrollBy(0, div.offsetHeight);
- }
- // Ensures that a quoted post's To link will work in its possibly
- // new context.
- function fixToLink(div)
- {
- var link = $xFirst('div[@class="n2"]/a[contains(., "To ")]', div);
- if (link) {
- var postNumber = link.hash.substr(1);
- if (document.anchors.namedItem(postNumber))
- return;
- link.href = 'posts?page=' + postNumber + '#' + postNumber;
- }
- }
- // Figures out where to put the quote box.
- function findAnchorDiv(node)
- {
- var p = node.parentNode;
- while (p.parentNode.tagName != 'BODY' && p.parentNode.id != 'posts')
- p = p.parentNode;
- do {
- p = p.previousSibling;
- } while (p && p.tagName != 'A');
- do {
- p = p.nextSibling;
- } while (p && p.tagName != 'DIV');
- return p;
- }
- // Hides a quoted post and scrolls the window to avoid disorienting
- // the user.
- function hideQuote(button)
- {
- if (button.value != 'Unquote')
- return;
- var maxScroll = window.scrollMaxY - window.scrollY;
- var totalHeight = 0;
- button.value = 'Quote';
- var quotedPost = button;
- var gp = quotedPost.parentNode.parentNode;
- if (gp.tagName != 'DIV' || gp.className != 'quoteBox')
- quotedPost = quotedPost.parentNode;
- else
- quotedPost = gp;
- do {
- quotedPost = quotedPost.previousSibling;
- } while (quotedPost && quotedPost.className != 'quoteBox');
- do {
- totalHeight += quotedPost.offsetHeight;
- var sibling = quotedPost.previousSibling;
- quotedPost.parentNode.removeChild(quotedPost);
- quotedPost = sibling;
- } while (quotedPost && quotedPost.tagName == 'DIV');
- window.scrollBy(0, -(totalHeight<maxScroll ? totalHeight : maxScroll));
- }
- // Locates the quote button in a quote box.
- function findQuoteButton(quoteDiv)
- {
- return $xFirst('div[@class="n2"]/input[@class="quoteButton"]', quoteDiv);
- }
- // -----------------------------------------------------------------------------
- // Constructs an object to keep track of the data about a poster in
- // the thread (for the Poster Report).
- function Poster(key, name, age, serial)
- {
- this.key = key; // To construct home page link
- this.name = name; // Display name
- this.sortKey = name.toLowerCase();
- this.age = age >= 0 ? age : 0; // How long on FR?
- this.serial = serial; // Order of first appearance on thread
- this.postCount = 0;
- this.replyCount = 0;
- }
- Poster.makeHeader = function(s)
- {
- s += '<tr>';
- s += '<th class=numh>Rank</th>';
- s += '<th>Poster</th>';
- s += '<th class=numh>FR<br>Age</th>';
- s += '<th class=numh>Posts</th>';
- s += '<th class=numh>Replies</th>';
- s += '<th class=numh>Replies<br>per Post</th>';
- s += '</tr>\n';
- return s;
- }
- Poster.prototype.calcReplyRatio = function()
- {
- this.replyRatio = this.replyCount / this.postCount;
- return this.replyRatio;
- }
- // Generates the HTML for a row of the Poster Report.
- Poster.prototype.makeRow = function(s, n)
- {
- s += '<tr><td class=num>' + n + '</td>';
- s += '<td><a href="http://www.freerepublic.com/~' + this.key + '/"' +
- ' target="_blank">';
- if (this.isOriginalPoster)
- s += '<b>';
- s += this.name;
- if (this.isOriginalPoster)
- s += '</b>';
- s += '</a></td>'
- s += '<td class=num>' + formatAge(this.age) + '</td>';
- s += '<td class=num>' + this.postCount + '</td>';
- s += '<td class=num>' + this.replyCount + '</td>';
- s += '<td class=num>' + (this.calcReplyRatio()).toFixed(1) + '</td>';
- s += '</tr>\n'
- return s;
- }
- function formatAge(age)
- {
- age /= 86400000;
- if (age > 360)
- return (age/365.25).toFixed(1) + 'y';
- if (age > 60)
- return (age/30).toFixed(1) + 'm';
- if (age > 21)
- return (age/7).toFixed(1) + 'w';
- return age.toFixed() + 'd';
- }
- var posters; // Table of Poster objects, accessed by name
- var posterList; // Sortable array (contents of posters)
- var posterWindow; // Window into which to write poster report
- var removedPostCount; // Count of admin-removed posts
- // Handles a click on the Poster Report button (or Ctrl-Alt-R).
- function onPosterReportClick(event, button)
- {
- button = button || this;
- disable(button, 'Waiting ...');
- disable('treeButton');
- openPosterWindow();
- window.setTimeout(function() {indexThread(button, makePosterReport)}, 0);
- }
- // Opens the poster report window. Must run in response to the user
- // click, not the multipage load completion, or else the popup will
- // be blocked (unless the user has added FR to the exception list).
- function openPosterWindow()
- {
- if (!posterWindow || posterWindow.closed)
- posterWindow = window.open('about:blank', 'posterReport');
- if (posterWindow) {
- posterWindow.blur();
- window.focus();
- }
- }
- // Builds the table of posters. Runs thru the posts index and the
- // replies and accumulates stats about each poster in his or her
- // Poster object.
- function makePosterReport(ok)
- {
- if (!ok) {
- enableAfterPosterReport();
- return;
- }
- var startDate = new Date();
- var now = new Date().getTime();
- posters = {};
- removedPostCount = 0;
- var regexp = new RegExp('<a href="/(?:%7E|~)([^/]*)/" title="' +
- 'Since (\\d\\d\\d\\d-\\d\\d-\\d\\d)">([^<]*)</a>', 'i');
- var list = [];
- for (var postNumber in posts)
- list.push(postNumber);
- list.sort(function(a, b) {return a-b;});
- for (var x=0, limit=list.length; x<limit; ++x) {
- var postNumber = list[x];
- var r = regexp.exec(posts[postNumber]);
- if (!r) {
- ++removedPostCount;
- continue;
- }
- var name = r[3];
- var poster = posters[name];
- if (!poster) {
- var date = Date.parse(r[2].replace(/-/g, '/') + ' GMT');
- poster = new Poster(r[1], name, now - date, x);
- posters[name] = poster;
- poster.isOriginalPoster = postNumber == 1;
- }
- ++poster.postCount;
- var replyList = replies[postNumber];
- if (replyList)
- poster.replyCount += replyList.length;
- }
- sortPostersBy('serial', false);
- try {
- showPosters();
- }
- catch(e) {
- alert("Error: " + e);
- }
- logTime(startDate, 'show the poster report on ' +
- posterList.length + ' posters');
- enableAfterPosterReport();
- }
- function enableAfterPosterReport()
- {
- enable('posterReport', 'Poster Report');
- enable('treeButton');
- }
- makePosterReport.title = 'poster report';
- // Displays the Poster Report in a popup window.
- function showPosters()
- {
- if (!posterWindow)
- return;
- var s = '<html><head><title>Poster Report</title>\n' +
- '<style>' +
- 'table {border-collapse:collapse;' +
- 'border:1px solid #00f;}' +
- 'td, th {border:1px inset #ccf;padding-left:3px;padding-right:3px}' +
- 'th {cursor:pointer;background:#ffc;vertical-align:bottom}' +
- 'h2 {color:darkred}' +
- '.numh {text-align:right}' +
- '.num {text-align:right;font:bold smaller monospace}\n' +
- '</style>\n' +
- '</head><body>\n' +
- '<h2>' + document.title + '</h2>\n' +
- '<h3>Poster Report</h3>\n';
- s += '<table>\n';
- s = Poster.makeHeader(s);
- var n = 0;
- var postCount = 0;
- for (var x=0, limit=posterList.length; x<limit; ++x) {
- var poster = posterList[x];
- s = poster.makeRow(s, ++n);
- postCount += poster.postCount;
- }
- s += '</table>\n';
- s += '<br>' + postCount + ' total posts, by ' + posterList.length +
- ' distinct posters. ' + (postCount/posterList.length).toFixed(1) +
- ' average posts per poster.\n';
- if (removedPostCount) {
- s += '<br>' + removedPostCount + ' post' +
- (removedPostCount == 1 ? ' was' : 's were') + ' removed.';
- }
- s += '<br>Average poster seniority: ' + formatAge(averageAge()) + '.';
- var div = posterWindow.document.createElement('div');
- div.innerHTML = s;
- var b = posterWindow.document.body;
- var oldDiv = b.firstChild;
- b.appendChild(div);
- if (oldDiv)
- b.removeChild(oldDiv);
- var xp = $x('.//th', posterWindow.document.body);
- for (var x=0, limit=xp.snapshotLength; x<limit; ++x)
- xp.snapshotItem(x).addEventListener('click', onHeaderClick, false);
- posterWindow.focus();
- }
- function averageAge()
- {
- var total = 0;
- for (var x=0, limit=posterList.length; x<limit; ++x)
- total += posterList[x].age;
- return total / limit;
- }
- // Receives control when the user clicks on a table header in the Poster
- // Report. Sorts the table by the selected column (or simply reverses it
- // if it's already sorted by that column). Then redisplays the report.
- function onHeaderClick()
- {
- var text = this.innerHTML;
- var parm = ['serial', false];
- if (/Poster/.test(text))
- parm = ['sortKey', false];
- else if (/Age/.test(text))
- parm = ['age', true];
- else if (/Posts/.test(text))
- parm = ['postCount', true];
- else if (/Replies$/.test(text))
- parm = ['replyCount', true];
- else if (/per Post/.test(text))
- parm = ['replyRatio', true];
- if (parm[0] == posterList.property)
- posterList.reverse();
- else
- sortPostersBy(parm[0], parm[1]);
- showPosters();
- }
- // Sorts the posters by the indicated Poster object property, in
- // ascending or descending order.
- function sortPostersBy(property, backwards)
- {
- posterList = [];
- posterList.property = property;
- for (name in posters)
- posterList.push(posters[name]);
- posterList.sort(comparator);
- function comparator(a, b) {
- var r;
- if (a[property] > b[property])
- r = 1;
- else if (a[property] < b[property])
- r = -1;
- else if (a.sortkey > b.sortKey)
- return 1;
- else if (a.sortKey < b.sortKey)
- return -1;
- else
- return 0;
- if (backwards)
- r = -r;
- return r;
- }
- }
- // -----------------------------------------------------------------------------
- function $(id) {return document.getElementById(id);}
- function $x(xpath, contextNode, resultType)
- {
- contextNode = contextNode || document.body;
- resultType = resultType || XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE;
- var doc = contextNode.ownerDocument; // FF3; can't just use document
- return doc.evaluate(xpath, contextNode, null, resultType, null);
- }
- function $xFirst(xpath, contextNode)
- {
- var xpr = $x(xpath, contextNode, XPathResult.FIRST_ORDERED_NODE_TYPE);
- return xpr.singleNodeValue;
- }
- // Disables a button and sets its text to the supplied string.
- function disable(button, msg)
- {
- if (typeof button == 'string')
- button = $(button);
- if (msg)
- button.value = msg;
- button.disabled = true;
- }
- // Enables a button and sets its text to the supplied string.
- function enable(button, msg)
- {
- if (typeof button == 'string')
- button = $(button);
- if (msg)
- button.value = msg;
- button.disabled = false;
- }
- // Adds the style rules defined in the STYLE constant to the page.
- function addStyles()
- {
- var style = document.createElement('style');
- style.innerHTML = STYLE;
- document.getElementsByTagName('head')[0].appendChild(style);
- }
- // Makes the link to the article to be discussed go directly to the
- // article without pausing and redirecting.
- function fixArticleRedirect()
- {
- var link = $xFirst('.//a[starts-with(@href, "/^")]');
- if (link)
- link.href = link.href.replace(/^.*?\%5E/, '');
- }
- // Removes those extra blank lines that seem to crop up at the end
- // of certain posts.
- function removeBlankLines(doc)
- {
- var list = $x('.//br[@clear="all"]', doc);
- for (var x=0, limit=list.snapshotLength; x<limit; ++x) {
- var br = list.snapshotItem(x);
- br.parentNode.removeChild(br);
- }
- }
- // Converts posting date stamps from Pacific to local time.
- function localizeDates(doc)
- {
- var list = $x('.//span[@class="date"]', doc);
- for (var x=0, limit=list.snapshotLength; x<limit; ++x) {
- var date = list.snapshotItem(x);
- date.innerHTML = new Date(date.innerHTML).toLocaleString();
- }
- }
- // Adds a Quote button next to the To link in each post having one.
- function addQuoteButtons(doc)
- {
- var quoteButtonModel = document.createElement('input');
- quoteButtonModel.className = 'quoteButton';
- quoteButtonModel.type = 'button';
- quoteButtonModel.value = 'Quote';
- var xp = $x('.//div[@class="n2"]/a[contains(., "To ")]', doc);
- for (var x=0, limit=xp.snapshotLength; x<limit; ++x) {
- var link = xp.snapshotItem(x);
- if (/^To \d+$/.test(link.innerHTML)) {
- var quoteButton = quoteButtonModel.cloneNode(true);
- quoteButton.addEventListener('mousedown', onQuotePress, false);
- link.parentNode.insertBefore(quoteButton, link.nextSibling);
- }
- }
- }
- // Adds the Tree View and Poster Report buttons at the top of the page,
- // next to the 'comments' link.
- function addButtons()
- {
- var node = $xFirst('//a[contains(@href, "#comment")]');
- node.innerHTML += ' <input type=button value="View as Tree" ' +
- 'id=treeButton title="Ctrl-Alt-T">';
- var span = document.createElement('span');
- span.innerHTML = ' <input type=button value="Poster Report" ' +
- 'id=posterReport title="Ctrl-Alt-R">';
- node.parentNode.appendChild(span);
- $('treeButton').addEventListener('click', onTreeViewClick, false);
- $('posterReport').addEventListener('click', onPosterReportClick, false);
- }
- // Installs a keystroke event handler to catch the keyboard shortcuts
- // for the Tree View and the Poster Report.
- function addKeys()
- {
- window.addEventListener('keypress', onKeyPress, false);
- }
- // Adds a blue box at the end of the page, in which to write debugging
- // messages.
- function addLogBox()
- {
- var div = document.createElement('div');
- div.id = 'logBox';
- document.body.appendChild(div);
- }
- // Writes a log message into the log box at the bottom of the page.
- function log(msg)
- {
- var logBox = $('logBox');
- var html = logBox.innerHTML;
- if (html)
- html += '<br>\n';
- logBox.innerHTML = html + msg;
- }
- // Logs the time it took to perform a given task.
- function logTime(startTime, task)
- {
- var time = new Date() - startTime;
- var s = 'It took ' + time + ' ms to ' + task;
- if (posts) {
- var n = posts.length - 1;
- s += ' for ' + n + ' posts; ' + (time/n).toFixed(2) +
- ' ms/post';
- }
- log(s + '.');
- }
- // Scrolls the window to the internal anchor indicated by the 'hash'.
- function fixLocation()
- {
- location.hash = location.hash;
- }
- // ------ Decrudify ------------------------------------------------------------
- // Decrudify fixes garbage characters introduced into posts by a recent
- // FR server bug in which the server breaks up the individual bytes
- // of posted UTF-8 sequences into their own separate HTML entities,
- // resulting in garbage characters on the screen.
- // E.g., a left curly double-quote is unicode \u201c, which takes three
- // bytes to represent in UTF-8: e2 80 9c. Instead of passing the UTF-8
- // unscathed, the server substitutes the entities for small-a with a
- // circumflex (e2), the euro-symbol (80), and the oe ligature (9c),
- // resulting in gibberish on the screen. Decrudify finds such garbage
- // sequences and substitutes the originally intended character.
- // A hash containing an entry for each entity value that converts to a
- // character in the range \u0080 thru \u00ff.
- var charCodeToByte = {
- 8364: 128,
- 129: 129,
- 8218: 130,
- 402: 131,
- 8222: 132,
- 8230: 133,
- 8224: 134,
- 8225: 135,
- 710: 136,
- 8240: 137,
- 352: 138,
- 8249: 139,
- 338: 140,
- 141: 141,
- 381: 142,
- 143: 143,
- 144: 144,
- 8216: 145,
- 8217: 146,
- 8220: 147,
- 8221: 148,
- 8226: 149,
- 8211: 150,
- 8212: 151,
- 732: 152,
- 8482: 153,
- 353: 154,
- 8250: 155,
- 339: 156,
- 157: 157,
- 382: 158,
- 376: 159,
- 160: 160,
- 161: 161,
- 162: 162,
- 163: 163,
- 164: 164,
- 165: 165,
- 166: 166,
- 167: 167,
- 168: 168,
- 169: 169,
- 170: 170,
- 171: 171,
- 172: 172,
- 173: 173,
- 174: 174,
- 175: 175,
- 176: 176,
- 177: 177,
- 178: 178,
- 179: 179,
- 180: 180,
- 181: 181,
- 182: 182,
- 183: 183,
- 184: 184,
- 185: 185,
- 186: 186,
- 187: 187,
- 188: 188,
- 189: 189,
- 190: 190,
- 191: 191,
- 192: 192,
- 193: 193,
- 194: 194,
- 195: 195,
- 196: 196,
- 197: 197,
- 198: 198,
- 199: 199,
- 200: 200,
- 201: 201,
- 202: 202,
- 203: 203,
- 204: 204,
- 205: 205,
- 206: 206,
- 207: 207,
- 208: 208,
- 209: 209,
- 210: 210,
- 211: 211,
- 212: 212,
- 213: 213,
- 214: 214,
- 215: 215,
- 216: 216,
- 217: 217,
- 218: 218,
- 219: 219,
- 220: 220,
- 221: 221,
- 222: 222,
- 223: 223,
- 224: 224,
- 225: 225,
- 226: 226,
- 227: 227,
- 228: 228,
- 229: 229,
- 230: 230,
- 231: 231,
- 232: 232,
- 233: 233,
- 234: 234,
- 235: 235,
- 236: 236,
- 237: 237,
- 238: 238,
- 239: 239,
- 240: 240,
- 241: 241,
- 242: 242,
- 243: 243,
- 244: 244,
- 245: 245,
- 246: 246,
- 247: 247,
- 248: 248,
- 249: 249,
- 250: 250,
- 251: 251,
- 252: 252,
- 253: 253,
- 254: 254,
- 255: 255,
- };
- // Makes a regular expression that matches UTF-8 sequences
- function makeUtf8Regex(table) {
- table = table || charCodeToByte;
- var keys = Object.getOwnPropertyNames(table).sort();
- var begRange = keys[0]*1;
- var prevKey = begRange;
- var regExp = '[' + toUnicodeLit(192) + '-' + toUnicodeLit(247) + '][';
- for (var x=1; x<keys.length; x++) {
- var c = keys[x]*1;
- if (c >= 192 && c <= 247)
- continue;
- if (c == prevKey + 1) {
- prevKey++;
- continue;
- }
- addToRegExp();
- begRange = c;
- prevKey = begRange;
- }
- addToRegExp();
- regExp += ']+';
- return new RegExp(regExp, 'g');
- function addToRegExp() {
- regExp += toUnicodeLit(begRange);
- if (begRange < prevKey)
- regExp += '-' + toUnicodeLit(prevKey);
- }
- }
- // Converts a numeric character code to a unicode hex literal (\uxxxx)
- function toUnicodeLit(charCode) {
- var hex = ('000' + charCode.toString(16)).slice(-4);
- return '\\u' + hex;
- }
- var regExp = makeUtf8Regex(charCodeToByte);
- // Returns the input string with any UTF-8 sequences converted to JavaScript
- // code points
- function utf8ToString(utf8) {
- fixCount++;
- var p = 0;
- var result = '';
- while (p < utf8.length) {
- var c = utf8.charCodeAt(p++);
- switch (c >> 4) {
- case 0:
- case 1:
- case 2:
- case 3:
- case 4:
- case 5:
- case 6:
- case 7:
- result += String.fromCharCode(c);
- break;
- case 8:
- case 9:
- case 10:
- case 11:
- console.log("Bad UTF-8 string: " + utf8.slice(p-16, p+16) +
- ' p = ' + p);
- break;
- case 12:
- case 13:
- result += String.fromCharCode((c & 31) << 6 |
- charCodeAt(utf8, p++));
- break;
- case 14:
- result += String.fromCharCode((c & 15) << 12 |
- charCodeAt(utf8, p++) << 6 |
- charCodeAt(utf8, p++));
- break;
- case 15:
- result += String.fromCodePoint((c & 7) << 18 |
- charCodeAt(utf8, p++) << 12 |
- charCodeAt(utf8, p++) << 6 |
- charCodeAt(utf8, p++));
- break;
- default:
- console.log("Bad charCode " + c + ' at p = ' + p + ' in ' +
- utf8);
- }
- }
- return result;
- }
- // Returns the byte value for the charCode at position x in the UTF-8 string.
- // E.g., charCode 8364 (the euro-sign) converts to 128
- function charCodeAt(utf8, x) {
- var c = charCodeToByte[utf8.charCodeAt(x)];
- if ((c >> 6) != 2)
- console.log("Bad UTF8 char " + c + " at " + x + " in " + utf8);
- return c & 63;
- }
- var fixCount;
- // Walks all the text nodes in the document and decrudifies each one
- function decrudify() {
- var t = performance.now();
- fixCount = 0;
- var nodeWalker = document.createTreeWalker(document.body,
- NodeFilter.SHOW_TEXT);
- while (nodeWalker.nextNode())
- decrudifyTextNode(nodeWalker.currentNode);
- t = performance.now() - t;
- var msg = "Decrudify made " + fixCount + " fix" +
- (fixCount == 1 ? '' : 'es') +
- ' in ' + t.toFixed(1) + "ms.";
- log(msg);
- console.log(msg);
- }
- // Runs utf8ToString on any UTF-8 sequences in a text node until no change
- // results. Replaces the text node if any change occurred
- function decrudifyTextNode(node) {
- var text = node.textContent;
- var originalText = text;
- while (true) {
- var newText = text.replace(regExp, utf8ToString);
- if (newText == text)
- break;
- text = newText;
- }
- if (newText != originalText)
- node.textContent = newText;
- }
- // -----------------------------------------------------------------------------
- // Main program ...
- var startTime = new Date();
- addStyles();
- addLogBox();
- fixArticleRedirect();
- decrudify();
- var originalHTML = document.body.innerHTML;
- removeBlankLines(document.body);
- addQuoteButtons(document.body);
- addButtons();
- addKeys();
- logTime(startTime, 'prepare the page');
- // Try to correct positioning error when going to internal anchors.
- if (location.hash) {
- fixLocation();
- document.body.addEventListener('load', fixLocation, false);
- window.setTimeout(fixLocation, 500);
- }