FR Tree Viewer

Brings together comments and their replies.

  1. // ==UserScript==
  2. // @name FR Tree Viewer
  3. // @namespace http://cynwoody.appspot.com/fr_tree_viewer.html
  4. // @description Brings together comments and their replies.
  5. // @date 2015-11-16
  6. // @include http://*.freerepublic.com*/posts*
  7. // @include http://freerepublic.com*/posts*
  8. // @version 0.0.1.20151118234707
  9. // ==/UserScript==
  10.  
  11. const STYLE = '.a2 {clear:both}' +
  12. '.postBox {' +
  13. 'padding:3px;' +
  14. 'border:1px solid #888;' +
  15. 'margin-top:3px;' +
  16. 'margin-bottom:3px}' +
  17. '.quoteButton {color:#009;margin-left:3px;padding:0px}' +
  18. '.quoteBox {background:#ffc;padding:3px;border:1px solid #ccf}' +
  19.  
  20. '#progressBox {position:fixed;left:2px;top:10px;' +
  21. 'background:#fc0;padding:3px;border:1px solid black}' +
  22. '#logBox {background:skyblue;padding:3px}' +
  23. '#scrollBox {position:fixed;top:0px;left:0px;z-index:1;' +
  24. 'border:1px solid #888;background:yellow}' +
  25. '#scrollButtons {padding:2px}' +
  26. '#scrollButtons input {padding:0px;margin:0px;color:blue}' +
  27. '#scrollClose, #scrollClose input {' +
  28. 'margin:0px;padding:0px;text-align:right;' +
  29. 'color:red;font-size:7px;font-weight:bold}';
  30.  
  31. const INDENT = 7; // Indentation per reply level, in pixels
  32. const MAX_INDENT = 0.7; // Maximum indent, as a fraction of window width
  33. const BACKGROUNDS = ['#ccf', '#ffc', '#cfc', '#fcc', '#cff', '#fcf', '#ccc'];
  34.  
  35. const SCROLL = {
  36. method: "constantTime", // Change to "jump" to eliminate animation
  37. constantSpeed: {
  38. pixelsPerInterval: 50,
  39. timeInterval: 10
  40. },
  41. constantTime: {
  42. intervalCount: 25,
  43. timeInterval: 20
  44. }
  45. };
  46.  
  47. var posts; // Maps postNumber to post (as HTML text)
  48. var replies; // Maps postNumber to a list of replying post numbers
  49. var postCheck; // Cross-check. Used to pick up deleted posts
  50. var maxIndent; // Maximum indent, in pixels
  51. var indexingCanceled; // Set when user cancels indexing of posts
  52. var selection; // Text selected when Quote button pressed
  53.  
  54. // Keystroke watcher. Implements keyboard shortcuts:
  55. // Ctrl-Alt-T => Tree View
  56. // Ctrl-Alt-R => Poster Report
  57.  
  58. function onKeyPress(event)
  59. {
  60. if (event.ctrlKey && event.altKey) {
  61. switch (event.charCode) {
  62. case 116: // t in Firefox
  63. case 20: // t in Chrome
  64. onTreeViewClick(event, $('treeButton'));
  65. break;
  66. case 114: // r in Firefox
  67. case 18: // r in Chrome
  68. onPosterReportClick(event, $('posterReport'));
  69. break;
  70. }
  71. }
  72. }
  73.  
  74. // Handles Tree View button click (or Ctrl-Alt-T).
  75.  
  76. function onTreeViewClick(event, button)
  77. {
  78. button = button || this;
  79. if (button.disabled)
  80. return;
  81. var callback = /Tree/.test(button.value) ? makeTreeView : makeFlatView;
  82. disable(button, 'Waiting ...');
  83. disable('posterReport');
  84. indexThread(button, callback);
  85. }
  86.  
  87. // Rearranges the posted comments into the tree view, assuming the
  88. // thread has been indexed successfully (ok == true).
  89.  
  90. function makeTreeView(ok)
  91. {
  92. if (!ok) {
  93. enable('treeButton', 'View as Tree');
  94. enable('posterReport');
  95. return;
  96. }
  97. var start = new Date();
  98. var div = deleteExistingPosts();
  99. maxIndent = window.innerWidth * MAX_INDENT;
  100. postCheck = {count:0};
  101. addReply(div, 1, 0);
  102. if (postCheck.count < posts.length-1)
  103. addDeletedPosts(div);
  104. if (postCheck.count != posts.length-1)
  105. alert('postCheck = ' + postCheck.count + ', but there are ' +
  106. (posts.length-1) + ' posts.');
  107. doFixups();
  108. addScrollBox();
  109. enable('treeButton', 'Flat View');
  110. enable('posterReport');
  111. logTime(start, 'generate the tree view');
  112. }
  113. makeTreeView.title = 'tree view';
  114.  
  115. // Makes a second pass over the posts to pick up deleted posts and
  116. // their replies. These would otherwise be missed, because deleted
  117. // posts have no To link.
  118.  
  119. function addDeletedPosts(container)
  120. {
  121. for (var x=1, limit=posts.length; x<limit; ++x) {
  122. if (!postCheck[x])
  123. addReply(container, x, 0);
  124. }
  125. }
  126.  
  127. // Adds a post to the display with the indentation indicated by
  128. // depth. Then calls itself to add each reply at the next deeper
  129. // indentation level.
  130.  
  131. function addReply(container, postNumber, depth)
  132. {
  133. ++postCheck.count;
  134. postCheck[postNumber] = true;
  135. var div = document.createElement('div');
  136. div.className = 'postBox';
  137. var indent = depth * INDENT;
  138. if (indent > maxIndent)
  139. indent = maxIndent;
  140. div.style.marginLeft = indent + 'px';
  141. div.style.background = BACKGROUNDS[depth % BACKGROUNDS.length];
  142. div.innerHTML = posts[postNumber];
  143. var anchor = document.createElement('a');
  144. anchor.name = postNumber;
  145. container.appendChild(anchor);
  146. container.appendChild(div);
  147. var replyList = replies[postNumber];
  148. if (replyList) {
  149. ++depth;
  150. for (var x=0, limit=replyList.length; x<limit; ++x)
  151. addReply(container, replyList[x], depth);
  152. }
  153. }
  154.  
  155. // Rearranges the posts into the chronological flat view, similar to
  156. // FR's normal view, but always showing all the posts.
  157.  
  158. function makeFlatView(ok)
  159. {
  160. if (!ok) {
  161. enable('treeButton', 'View as Tree');
  162. enable('posterReport');
  163. return;
  164. }
  165. var start = new Date();
  166. var div = deleteExistingPosts();
  167. var s = '';
  168. for (var postNumber=1, limit=posts.length; postNumber<limit; ++postNumber) {
  169. s += '<a name=' + postNumber + '></a>\n';
  170. s += posts[postNumber];
  171. s += '<hr size=1 noshade=noshade>\n';
  172. }
  173. div.innerHTML = s;
  174. removeScrollBox();
  175. doFixups();
  176. enable('treeButton', 'Tree View');
  177. enable('posterReport');
  178. logTime(start, 'generate the flat view');
  179. }
  180. makeTreeView.title = 'flat view';
  181.  
  182. // Performs fixups needed after view creation (tree or flat).
  183.  
  184. function doFixups()
  185. {
  186. fixLinks(document.body);
  187. removeBlankLines(document.body);
  188. addQuoteButtons(document.body);
  189. localizeDates(document.body);
  190. decrudify();
  191. }
  192.  
  193. // Rewrites all the To-links in the node in such a was as to ensure
  194. // they are valid in the new, single-page environment.
  195.  
  196. function fixLinks(node)
  197. {
  198. var links = $x('.//a[contains(., "To ")]', node);
  199. for (var x=0, limit=links.snapshotLength; x<limit; ++x) {
  200. var link = links.snapshotItem(x);
  201. link.href = link.hash;
  202. }
  203. }
  204.  
  205. // Removes the existing posts and replaces them with an empty div
  206. // ready to receive the rearranged posts. Returns the empty div.
  207.  
  208. function deleteExistingPosts()
  209. {
  210. var firstNode = findBeginningOfPosts();
  211. var lastNode = document.body.lastChild;
  212. do {
  213. lastNode = lastNode.previousSibling;
  214. } while (!/Disclaimer/.test(lastNode.innerHTML));
  215. var range = document.createRange();
  216. range.setStartAfter(firstNode);
  217. range.setEndBefore(lastNode);
  218. range.deleteContents();
  219. range.detach();
  220. var div = document.createElement('div');
  221. div.id = 'posts';
  222. document.body.insertBefore(div, lastNode);
  223. return div;
  224. }
  225.  
  226. // Locates the node before the first post. If the user is signed in,
  227. // we can use the 'comment' anchor. Otherwise, we have to find the
  228. // actual first post and back up to the horizontal rule.
  229.  
  230. function findBeginningOfPosts()
  231. {
  232. var firstNode = document.anchors.namedItem('comment');
  233. if (firstNode)
  234. return firstNode;
  235. var node = $xFirst('div[@class="b2"]');
  236. node = node || $xFirst('div[@id="posts"]');
  237. while (node) {
  238. node = node.previousSibling;
  239. if (node.tagName == 'HR')
  240. return node;
  241. }
  242. alert("Can't find posts!");
  243. return null;
  244. }
  245.  
  246. // Parses all the posts in the thread and builds two tables (unless
  247. // they already exist):
  248. // - posts, an array that maps a postNumber to the post's HTML
  249. // snippet.
  250. // - replies, a hash that links a postNumber to an array of
  251. // replying postNumbers.
  252. // Calls the callback function when the indexing is complete (it will
  253. // happen asynchronously if page fetches are required).
  254.  
  255. function indexThread(button, callback)
  256. {
  257. if (replies)
  258. callback(true);
  259. else {
  260. var first = $xFirst('a[@class="fr_page_goto"][contains(., "first")]');
  261. if (first) {
  262. button.value = 'Waiting ...';
  263. indexWholeThread(first, callback);
  264. }
  265. else {
  266. var start = new Date();
  267. indexPosts(originalHTML);
  268. indexReplies();
  269. var now = new Date();
  270. logTime(start, 'index the thread');
  271. callback(true);
  272. }
  273. }
  274. }
  275.  
  276. // Called by indexThread to index multipage threads. Fetches the entire
  277. // thread in the background, adding the posts of each page to the posts
  278. // index. Then indexes the replies and calls the callback function,
  279. // indicating whether the operation completed or was canceled by the
  280. // user.
  281.  
  282. function indexWholeThread(firstLink, callback)
  283. {
  284. var start = new Date();
  285. var link = document.createElement('a');
  286. link.href = firstLink.href;
  287. link.hash = '';
  288. loadLink(link, loadNext);
  289.  
  290. function loadNext(html)
  291. {
  292. if (indexingCanceled) {
  293. indexingCanceled = posts = replies = null;
  294. hideProgress();
  295. log('Loading and indexing canceled!');
  296. callback(false);
  297. return;
  298. }
  299. if (!html) {
  300. callback(false);
  301. return;
  302. }
  303. var r = html.match(/href="posts(\?[^"]*)" class="fr_page_goto"[^>]*>next</);
  304. if (r) {
  305. link.search = r[1];
  306. loadLink(link, loadNext);
  307. }
  308. else {
  309. showProgress('Generating ' + callback.title + ' ...');
  310. var postCount = posts.length - 1;
  311. indexReplies();
  312. var now = new Date();
  313. logTime(start, 'load and index');
  314. callback(true);
  315. hideProgress();
  316. }
  317. }
  318. }
  319.  
  320. // Displays a message in floating box to let the user know how far a
  321. // multipage indexing operation has proceeded. The box includes a Cancel
  322. // button, in case the user decides to bail.
  323.  
  324. function showProgress(msg)
  325. {
  326. var progress = $('progressMsg');
  327. if (!progress)
  328. progress = makeProgressBox();
  329. progress.innerHTML = msg;
  330. $('indexingCancel').disabled = false;
  331. $('progressBox').style.display = 'block';
  332. }
  333.  
  334. function makeProgressBox()
  335. {
  336. var div = document.createElement('div');
  337. div.id = 'progressBox';
  338. div.innerHTML = '<span id=progressMsg></span> ' +
  339. '<input id=indexingCancel type=button value=Cancel>';
  340. document.body.appendChild(div);
  341. $('indexingCancel').addEventListener('click', onProgressCancel, false);
  342. return $('progressMsg');
  343. }
  344.  
  345. function hideProgress()
  346. {
  347. var box = $('progressBox');
  348. if (box)
  349. box.style.display = 'none';
  350. }
  351.  
  352. // Handles a progress box cancel click. Signals the indexing to stop.
  353.  
  354. function onProgressCancel()
  355. {
  356. indexingCanceled = true;
  357. $('indexingCancel').disabled = true;
  358. }
  359.  
  360. // Fetches a page in the background, indexes it, and calls the callback,
  361. // passing the retrieved HTML.
  362.  
  363. function loadLink(link, callback)
  364. {
  365. var req = new XMLHttpRequest();
  366. req.open('GET', link, true);
  367. req.onreadystatechange = handler;
  368. req.send(null);
  369. showProgress('Loading ' + link);
  370.  
  371. function handler()
  372. {
  373. if (req.readyState == 4) {
  374. var html = req.responseText;
  375. if (req.status != 200) {
  376. var msg = "XHR received " + req.status + ' ' + req.statusText +
  377. ' loading ' + link + '.';
  378. alert(msg);
  379. log(msg);
  380. html = null;
  381. indexingCanceled = true;
  382. }
  383. if (html)
  384. indexPosts(html);
  385. callback(html);
  386. }
  387. }
  388. }
  389.  
  390. // Extracts the posts from a page of HTML text and adds them to the
  391. // posts array, using postNumber as the subscript.
  392.  
  393. function indexPosts(html)
  394. {
  395. var r = new RegExp('<a name="(\\d+)"></a>\\n([\\s\\S]+?' +
  396. '(?:<div class="n2">[\\s\\S]+?</div>|' +
  397. 'Moderator</i></small><br[ /]*>))', 'gi');
  398. posts = posts || [];
  399. var match;
  400. while (match = r.exec(html))
  401. posts[match[1]] = match[2];
  402. }
  403.  
  404. // Runs thru the posts array and builds the replies table. The replies
  405. // table contains an array of replying post numbers for each post that
  406. // has at least one reply.
  407.  
  408. function indexReplies()
  409. {
  410. replies = {};
  411. for (var postNumber=1, limit=posts.length; postNumber<limit; ++postNumber) {
  412. var m = /<a .*?href=".*?#(\d+)">To \1</i.exec(posts[postNumber]);;
  413. if (m && m[1]) {
  414. var toNumber = m[1];
  415. var replyList = replies[toNumber];
  416. if (replyList)
  417. replyList.push(postNumber);
  418. else
  419. replies[toNumber] = [postNumber];
  420. }
  421. }
  422. }
  423.  
  424. // Adds a draggable floating box which appears when the user clicks in
  425. // in the white indentation space of the tree view. The box includes
  426. // buttons to scroll up or down to the next post at or above the box's
  427. // indent level.
  428.  
  429. function addScrollBox()
  430. {
  431. var div = document.createElement('div');
  432. div.id = 'scrollBox';
  433. div.style.display = 'none';
  434. div.innerHTML = '<div id=scrollClose>' +
  435. '<input id=scrollCloseButton type=button value=x></div>' +
  436. '<div id=scrollButtons>' +
  437. '<input id=up type=button value=Up><br>' +
  438. '<input id=dn type=button value=Dn>' +
  439. '</div>';
  440. div.title = 'Scrolls up or down to next comment of same or outer color. ' +
  441. 'Drag to change color.';
  442. document.body.appendChild(div);
  443. $('posts').addEventListener('click', onPostsClick, false);
  444. $('scrollCloseButton').addEventListener('click', onScrollClose, false);
  445. var list = $x('.//input', div);
  446. for (var x=0, limit=list.snapshotLength; x<limit; ++x) {
  447. var button = list.snapshotItem(x);
  448. button.addEventListener('mousedown', onScrollButtonMouseDown, false);
  449. }
  450. div.addEventListener('mousedown', onScrollBoxMouseDown, false);
  451. $('dn').addEventListener('click', onDnClick, false);
  452. $('up').addEventListener('click', onUpClick, false);
  453. }
  454.  
  455. // Deletes the scroll box created by addScrollBox, if it exists.
  456.  
  457. function removeScrollBox()
  458. {
  459. var scrollBox = $('scrollBox');
  460. if (scrollBox) {
  461. var posts = scrollBox.parentNode;
  462. posts.removeChild(scrollBox);
  463. posts.removeEventListener('click', onPostsClick, false);
  464. }
  465. }
  466.  
  467. // Responds to a click in the tree view indentation white space by
  468. // showing the scroll box at the spot clicked.
  469.  
  470. function onPostsClick(e)
  471. {
  472. if (e.target.id != 'posts')
  473. return;
  474. var scrollBox = $('scrollBox');
  475. scrollBox.style.display = 'block';
  476. scrollBox.style.left = (e.clientX - scrollBox.offsetWidth/2) + 'px';
  477. scrollBox.style.top = e.clientY + 'px';
  478. colorScrollBox(scrollBox);
  479. }
  480.  
  481. // Hides the scroll box when the user clicks its Close button.
  482.  
  483. function onScrollClose(e)
  484. {
  485. $('scrollBox').style.display = 'none';
  486. }
  487.  
  488. // Traps mousedowns on scroll box buttons, so they won't start a drag.
  489.  
  490. function onScrollButtonMouseDown(e)
  491. {
  492. e.stopPropagation();
  493. }
  494.  
  495. // Supports dragging the scroll box by its white space areas.
  496.  
  497. function onScrollBoxMouseDown(e)
  498. {
  499. this.addEventListener('mousemove', onMouseMove, true);
  500. this.addEventListener('mouseup', onMouseUp, true);
  501. var mdx = e.clientX;
  502. var mdy = e.clientY;
  503. var mdLeft = parseInt(this.style.left);
  504. var mdTop = parseInt(this.style.top);
  505. e.stopPropagation();
  506.  
  507. function onMouseMove(e)
  508. {
  509. var x = e.clientX - mdx;
  510. var y = e.clientY - mdy;
  511. this.style.left = mdLeft + x + 'px';
  512. this.style.top = mdTop + y + 'px';
  513. colorScrollBox(this);
  514. e.stopPropagation();
  515. }
  516.  
  517. function onMouseUp(e)
  518. {
  519. this.removeEventListener('mousemove', onMouseMove, true);
  520. this.removeEventListener('mouseup', onMouseUp, true);
  521. e.stopPropagation();
  522. }
  523. }
  524.  
  525. // Sets the scroll box's background color to correspond to its current
  526. // indent level.
  527.  
  528. function colorScrollBox(scrollBox)
  529. {
  530. var depth = scrollBoxDepth(scrollBox);
  531. scrollBox.style.background = BACKGROUNDS[depth % BACKGROUNDS.length];
  532. }
  533.  
  534. // Computes the scroll box's indent level, based on its horizontal
  535. // position.
  536.  
  537. function scrollBoxDepth(scrollBox)
  538. {
  539. var offset = scrollBox.offsetLeft - $('posts').offsetLeft +
  540. Math.floor(scrollBox.offsetWidth/2);
  541. return offset > 0 ? Math.floor(offset/INDENT) : 0;
  542. }
  543.  
  544. // Scrolls up to the next post at or left of the scroll box's indent
  545. // level.
  546.  
  547. function onUpClick(event)
  548. {
  549. scrollPosts(event, findUp);
  550.  
  551. function findUp(boxDepth, boxTop, divs)
  552. {
  553. for (var x=divs.snapshotLength-1; x>=0; --x) {
  554. var div = divs.snapshotItem(x);
  555. if (div.offsetTop >= boxTop)
  556. continue;
  557. var divDepth = parseInt(div.style.marginLeft) / INDENT;
  558. if (divDepth <= boxDepth)
  559. break;
  560. }
  561. return div;
  562. }
  563. }
  564.  
  565. // Scrolls down to the next post at or left of the scroll box's indent
  566. // level.
  567.  
  568. function onDnClick(event)
  569. {
  570. scrollPosts(event, findDown);
  571.  
  572. function findDown(boxDepth, boxTop, divs)
  573. {
  574. for (var x=0, limit=divs.snapshotLength; x<limit; ++x) {
  575. var div = divs.snapshotItem(x);
  576. if (div.offsetTop <= boxTop)
  577. continue;
  578. var divDepth = parseInt(div.style.marginLeft) / INDENT;
  579. if (divDepth <= boxDepth)
  580. break;
  581. }
  582. return div;
  583. }
  584. }
  585.  
  586. // Scrolls the display so that the post found by the findDiv function
  587. // is opposite the scroll box. Chooses between three different scroll
  588. // methods, depending on the settings in the SCROLL constant. Available
  589. // methods include two types of animation and a simple jump. If the
  590. // control or shift key is down, it always uses the jump method.
  591.  
  592. function scrollPosts(event, findDiv)
  593. {
  594. var box = $('scrollBox');
  595. var boxTop = box.offsetTop + window.pageYOffset;
  596. var boxDepth = scrollBoxDepth(box);
  597. var divs = $x('div', $('posts'));
  598. var div = findDiv(boxDepth, boxTop, divs);
  599. var scrollDistance = div.offsetTop - boxTop;
  600.  
  601. if (/jump/i.test(SCROLL.method) || event.ctrlKey || event.shiftKey)
  602. jump();
  603. else if (/time/i.test(SCROLL.method))
  604. constantTime();
  605. else if (/speed/i.test(SCROLL.method))
  606. constantSpeed();
  607. else
  608. constantTime();
  609.  
  610. // Non-animated, simple scroll.
  611.  
  612. function jump()
  613. {
  614. window.scrollBy(0, scrollDistance);
  615. }
  616.  
  617. var interval;
  618.  
  619. // Animated scroll: Moves the display faster or slower depending
  620. // on the distance to scroll.
  621.  
  622. function constantTime()
  623. {
  624. var parms = SCROLL.constantTime;
  625. var intervalCount = parms.intervalCount;
  626. interval = window.setInterval(scrollABit, parms.timeInterval);
  627. document.body.addEventListener('click', abort, true);
  628.  
  629. function scrollABit()
  630. {
  631. if (intervalCount == 0) {
  632. abort();
  633. return;
  634. }
  635. var scrollInc = Math.round(scrollDistance / intervalCount--);
  636. window.scrollBy(0, scrollInc);
  637. scrollDistance -= scrollInc;
  638. }
  639. }
  640.  
  641. // Animated scroll: Moves the display at a steady speed until the
  642. // distance is covered.
  643.  
  644. function constantSpeed()
  645. {
  646. var parms = SCROLL.constantSpeed;
  647. var scrollInc = parms.pixelsPerInterval;
  648. if (scrollDistance < 0)
  649. scrollInc = -scrollInc;
  650. interval = window.setInterval(scrollABit, parms.timeInterval);
  651. document.body.addEventListener('click', abort, true);
  652.  
  653. function scrollABit()
  654. {
  655. if (scrollDistance == 0) {
  656. abort();
  657. return;
  658. }
  659. if (Math.abs(scrollInc) > Math.abs(scrollDistance))
  660. scrollInc = scrollDistance;
  661. window.scrollBy(0, scrollInc);
  662. scrollDistance -= scrollInc;
  663. }
  664.  
  665. }
  666.  
  667. // Terminates an animated scroll early if the user clicks.
  668.  
  669. function abort()
  670. {
  671. window.clearInterval(interval);
  672. document.body.removeEventListener('click', arguments.callee, true);
  673. }
  674. }
  675.  
  676. // -----------------------------------------------------------------------------
  677.  
  678. // Handles a mouse press on a Quote button. Installs a click handler for
  679. // the button, allowing the click to be handled correctly while preserving
  680. // any text selection the user may have made.
  681.  
  682. function onQuotePress()
  683. {
  684. this.removeEventListener('click', onQuoteClick, false);
  685. this.addEventListener('click', onQuoteClick, false);
  686. var sel = window.getSelection();
  687. selection = sel.toString();
  688. sel.removeAllRanges(); // Deselect the text
  689. }
  690.  
  691. // Handles a Quote / Unquote button click. If there is already a quoted
  692. // post showing (the Unquote button case), we delete it. Otherwise (the
  693. // Quote button case), we locate the post to be quoted and display it
  694. // in a box above the current post (the one containing the Quote button).
  695. // Unless a post number has been selected with the mouse, we quote the
  696. // the post to which the current post is in reply.
  697.  
  698. // If the desired post is not in memory, we will load the page that
  699. // contains it in the background, while showing the progress bar.
  700.  
  701. function onQuoteClick()
  702. {
  703. var button = this;
  704. if (selection) {
  705. var postNumbers = selection.match(/\d+/g);
  706. if (postNumbers) {
  707. hideQuote(button);
  708. quoteSelectedPostNumbers(button, postNumbers);
  709. return;
  710. }
  711. }
  712. if (button.value == 'Unquote') {
  713. hideQuote(button);
  714. return;
  715. }
  716. var postNumber = button.previousSibling.hash.substr(1);
  717. findAndQuotePost(button, postNumber);
  718. }
  719.  
  720. // Finds and quotes each of a list of posts
  721.  
  722. function quoteSelectedPostNumbers(button, postNumbers)
  723. {
  724. for (var x=0; x<postNumbers.length; ++x)
  725. findAndQuotePost(button, postNumbers[x]);
  726. }
  727.  
  728. // Finds and quotes a given post. Runs right away if the desired post
  729. // is in memory. Otherwise, it loads the page containing the post in
  730. // the background before continuing.
  731.  
  732. function findAndQuotePost(button, postNumber)
  733. {
  734. if (findPost(postNumber))
  735. quotePost(button, postNumber);
  736. else {
  737. disable(button, 'Waiting ...');
  738. var l = window.location;
  739. var url = l.protocol + '//' + l.hostname + l.pathname +
  740. '?page=' + postNumber + '#' + postNumber;
  741. loadLink(url, function() {
  742. quotePost(button, postNumber);
  743. });
  744. button.disabled = false;
  745. }
  746. }
  747.  
  748. // Looks for the post in the posts index. If there is no posts index,
  749. // we build one for the current page. Returns undefined if not found.
  750.  
  751. function findPost(postNumber)
  752. {
  753. hideProgress();
  754. if (!posts)
  755. indexPosts(originalHTML);
  756. return posts[postNumber];
  757. }
  758.  
  759. // Copies the post to be quoted into a box above the post containing
  760. // the Quote button.
  761.  
  762. function quotePost(button, postNumber)
  763. {
  764. var post = findPost(postNumber);
  765. post = post || '<b>Unable to retrieve post #' + postNumber + '.</b>';
  766. var div = button.ownerDocument.createElement('div');
  767. div.className = 'quoteBox';
  768. div.innerHTML = post;
  769. addQuoteButtons(div);
  770. removeBlankLines(div);
  771. fixToLink(div);
  772. localizeDates(div);
  773. var anchorDiv = findAnchorDiv(button);
  774. if (anchorDiv.className != 'postBox')
  775. anchorDiv.parentNode.insertBefore(div, anchorDiv);
  776. else
  777. anchorDiv.insertBefore(div, anchorDiv.firstChild);
  778. button.value = 'Unquote';
  779. window.scrollBy(0, div.offsetHeight);
  780. }
  781.  
  782. // Ensures that a quoted post's To link will work in its possibly
  783. // new context.
  784.  
  785. function fixToLink(div)
  786. {
  787. var link = $xFirst('div[@class="n2"]/a[contains(., "To ")]', div);
  788. if (link) {
  789. var postNumber = link.hash.substr(1);
  790. if (document.anchors.namedItem(postNumber))
  791. return;
  792. link.href = 'posts?page=' + postNumber + '#' + postNumber;
  793. }
  794. }
  795.  
  796. // Figures out where to put the quote box.
  797.  
  798. function findAnchorDiv(node)
  799. {
  800. var p = node.parentNode;
  801. while (p.parentNode.tagName != 'BODY' && p.parentNode.id != 'posts')
  802. p = p.parentNode;
  803. do {
  804. p = p.previousSibling;
  805. } while (p && p.tagName != 'A');
  806. do {
  807. p = p.nextSibling;
  808. } while (p && p.tagName != 'DIV');
  809. return p;
  810. }
  811.  
  812. // Hides a quoted post and scrolls the window to avoid disorienting
  813. // the user.
  814.  
  815. function hideQuote(button)
  816. {
  817. if (button.value != 'Unquote')
  818. return;
  819. var maxScroll = window.scrollMaxY - window.scrollY;
  820. var totalHeight = 0;
  821. button.value = 'Quote';
  822. var quotedPost = button;
  823. var gp = quotedPost.parentNode.parentNode;
  824. if (gp.tagName != 'DIV' || gp.className != 'quoteBox')
  825. quotedPost = quotedPost.parentNode;
  826. else
  827. quotedPost = gp;
  828. do {
  829. quotedPost = quotedPost.previousSibling;
  830. } while (quotedPost && quotedPost.className != 'quoteBox');
  831. do {
  832. totalHeight += quotedPost.offsetHeight;
  833. var sibling = quotedPost.previousSibling;
  834. quotedPost.parentNode.removeChild(quotedPost);
  835. quotedPost = sibling;
  836. } while (quotedPost && quotedPost.tagName == 'DIV');
  837. window.scrollBy(0, -(totalHeight<maxScroll ? totalHeight : maxScroll));
  838. }
  839.  
  840. // Locates the quote button in a quote box.
  841.  
  842. function findQuoteButton(quoteDiv)
  843. {
  844. return $xFirst('div[@class="n2"]/input[@class="quoteButton"]', quoteDiv);
  845. }
  846.  
  847. // -----------------------------------------------------------------------------
  848.  
  849. // Constructs an object to keep track of the data about a poster in
  850. // the thread (for the Poster Report).
  851.  
  852. function Poster(key, name, age, serial)
  853. {
  854. this.key = key; // To construct home page link
  855. this.name = name; // Display name
  856. this.sortKey = name.toLowerCase();
  857. this.age = age >= 0 ? age : 0; // How long on FR?
  858. this.serial = serial; // Order of first appearance on thread
  859. this.postCount = 0;
  860. this.replyCount = 0;
  861. }
  862.  
  863. Poster.makeHeader = function(s)
  864. {
  865. s += '<tr>';
  866. s += '<th class=numh>Rank</th>';
  867. s += '<th>Poster</th>';
  868. s += '<th class=numh>FR<br>Age</th>';
  869. s += '<th class=numh>Posts</th>';
  870. s += '<th class=numh>Replies</th>';
  871. s += '<th class=numh>Replies<br>per Post</th>';
  872. s += '</tr>\n';
  873. return s;
  874. }
  875.  
  876. Poster.prototype.calcReplyRatio = function()
  877. {
  878. this.replyRatio = this.replyCount / this.postCount;
  879. return this.replyRatio;
  880. }
  881.  
  882. // Generates the HTML for a row of the Poster Report.
  883.  
  884. Poster.prototype.makeRow = function(s, n)
  885. {
  886. s += '<tr><td class=num>' + n + '</td>';
  887. s += '<td><a href="http://www.freerepublic.com/~' + this.key + '/"' +
  888. ' target="_blank">';
  889. if (this.isOriginalPoster)
  890. s += '<b>';
  891. s += this.name;
  892. if (this.isOriginalPoster)
  893. s += '</b>';
  894. s += '</a></td>'
  895. s += '<td class=num>' + formatAge(this.age) + '</td>';
  896. s += '<td class=num>' + this.postCount + '</td>';
  897. s += '<td class=num>' + this.replyCount + '</td>';
  898. s += '<td class=num>' + (this.calcReplyRatio()).toFixed(1) + '</td>';
  899. s += '</tr>\n'
  900. return s;
  901. }
  902.  
  903. function formatAge(age)
  904. {
  905. age /= 86400000;
  906. if (age > 360)
  907. return (age/365.25).toFixed(1) + 'y';
  908. if (age > 60)
  909. return (age/30).toFixed(1) + 'm';
  910. if (age > 21)
  911. return (age/7).toFixed(1) + 'w';
  912. return age.toFixed() + 'd';
  913. }
  914.  
  915. var posters; // Table of Poster objects, accessed by name
  916. var posterList; // Sortable array (contents of posters)
  917. var posterWindow; // Window into which to write poster report
  918. var removedPostCount; // Count of admin-removed posts
  919.  
  920. // Handles a click on the Poster Report button (or Ctrl-Alt-R).
  921.  
  922. function onPosterReportClick(event, button)
  923. {
  924. button = button || this;
  925. disable(button, 'Waiting ...');
  926. disable('treeButton');
  927. openPosterWindow();
  928. window.setTimeout(function() {indexThread(button, makePosterReport)}, 0);
  929. }
  930.  
  931. // Opens the poster report window. Must run in response to the user
  932. // click, not the multipage load completion, or else the popup will
  933. // be blocked (unless the user has added FR to the exception list).
  934.  
  935. function openPosterWindow()
  936. {
  937. if (!posterWindow || posterWindow.closed)
  938. posterWindow = window.open('about:blank', 'posterReport');
  939. if (posterWindow) {
  940. posterWindow.blur();
  941. window.focus();
  942. }
  943. }
  944.  
  945. // Builds the table of posters. Runs thru the posts index and the
  946. // replies and accumulates stats about each poster in his or her
  947. // Poster object.
  948.  
  949. function makePosterReport(ok)
  950. {
  951. if (!ok) {
  952. enableAfterPosterReport();
  953. return;
  954. }
  955. var startDate = new Date();
  956. var now = new Date().getTime();
  957. posters = {};
  958. removedPostCount = 0;
  959. var regexp = new RegExp('<a href="/(?:%7E|~)([^/]*)/" title="' +
  960. 'Since (\\d\\d\\d\\d-\\d\\d-\\d\\d)">([^<]*)</a>', 'i');
  961. var list = [];
  962. for (var postNumber in posts)
  963. list.push(postNumber);
  964. list.sort(function(a, b) {return a-b;});
  965. for (var x=0, limit=list.length; x<limit; ++x) {
  966. var postNumber = list[x];
  967. var r = regexp.exec(posts[postNumber]);
  968. if (!r) {
  969. ++removedPostCount;
  970. continue;
  971. }
  972. var name = r[3];
  973. var poster = posters[name];
  974. if (!poster) {
  975. var date = Date.parse(r[2].replace(/-/g, '/') + ' GMT');
  976. poster = new Poster(r[1], name, now - date, x);
  977. posters[name] = poster;
  978. poster.isOriginalPoster = postNumber == 1;
  979. }
  980. ++poster.postCount;
  981. var replyList = replies[postNumber];
  982. if (replyList)
  983. poster.replyCount += replyList.length;
  984. }
  985. sortPostersBy('serial', false);
  986. try {
  987. showPosters();
  988. }
  989. catch(e) {
  990. alert("Error: " + e);
  991. }
  992. logTime(startDate, 'show the poster report on ' +
  993. posterList.length + ' posters');
  994. enableAfterPosterReport();
  995. }
  996.  
  997. function enableAfterPosterReport()
  998. {
  999. enable('posterReport', 'Poster Report');
  1000. enable('treeButton');
  1001. }
  1002.  
  1003. makePosterReport.title = 'poster report';
  1004.  
  1005. // Displays the Poster Report in a popup window.
  1006.  
  1007. function showPosters()
  1008. {
  1009. if (!posterWindow)
  1010. return;
  1011. var s = '<html><head><title>Poster Report</title>\n' +
  1012. '<style>' +
  1013. 'table {border-collapse:collapse;' +
  1014. 'border:1px solid #00f;}' +
  1015. 'td, th {border:1px inset #ccf;padding-left:3px;padding-right:3px}' +
  1016. 'th {cursor:pointer;background:#ffc;vertical-align:bottom}' +
  1017. 'h2 {color:darkred}' +
  1018.  
  1019. '.numh {text-align:right}' +
  1020. '.num {text-align:right;font:bold smaller monospace}\n' +
  1021. '</style>\n' +
  1022. '</head><body>\n' +
  1023. '<h2>' + document.title + '</h2>\n' +
  1024. '<h3>Poster Report</h3>\n';
  1025. s += '<table>\n';
  1026. s = Poster.makeHeader(s);
  1027. var n = 0;
  1028. var postCount = 0;
  1029. for (var x=0, limit=posterList.length; x<limit; ++x) {
  1030. var poster = posterList[x];
  1031. s = poster.makeRow(s, ++n);
  1032. postCount += poster.postCount;
  1033. }
  1034. s += '</table>\n';
  1035. s += '<br>' + postCount + ' total posts, by ' + posterList.length +
  1036. ' distinct posters. ' + (postCount/posterList.length).toFixed(1) +
  1037. ' average posts per poster.\n';
  1038. if (removedPostCount) {
  1039. s += '<br>' + removedPostCount + ' post' +
  1040. (removedPostCount == 1 ? ' was' : 's were') + ' removed.';
  1041. }
  1042. s += '<br>Average poster seniority: ' + formatAge(averageAge()) + '.';
  1043. var div = posterWindow.document.createElement('div');
  1044. div.innerHTML = s;
  1045. var b = posterWindow.document.body;
  1046. var oldDiv = b.firstChild;
  1047. b.appendChild(div);
  1048. if (oldDiv)
  1049. b.removeChild(oldDiv);
  1050. var xp = $x('.//th', posterWindow.document.body);
  1051. for (var x=0, limit=xp.snapshotLength; x<limit; ++x)
  1052. xp.snapshotItem(x).addEventListener('click', onHeaderClick, false);
  1053. posterWindow.focus();
  1054. }
  1055.  
  1056. function averageAge()
  1057. {
  1058. var total = 0;
  1059. for (var x=0, limit=posterList.length; x<limit; ++x)
  1060. total += posterList[x].age;
  1061. return total / limit;
  1062. }
  1063.  
  1064. // Receives control when the user clicks on a table header in the Poster
  1065. // Report. Sorts the table by the selected column (or simply reverses it
  1066. // if it's already sorted by that column). Then redisplays the report.
  1067.  
  1068. function onHeaderClick()
  1069. {
  1070. var text = this.innerHTML;
  1071. var parm = ['serial', false];
  1072. if (/Poster/.test(text))
  1073. parm = ['sortKey', false];
  1074. else if (/Age/.test(text))
  1075. parm = ['age', true];
  1076. else if (/Posts/.test(text))
  1077. parm = ['postCount', true];
  1078. else if (/Replies$/.test(text))
  1079. parm = ['replyCount', true];
  1080. else if (/per Post/.test(text))
  1081. parm = ['replyRatio', true];
  1082. if (parm[0] == posterList.property)
  1083. posterList.reverse();
  1084. else
  1085. sortPostersBy(parm[0], parm[1]);
  1086. showPosters();
  1087. }
  1088.  
  1089. // Sorts the posters by the indicated Poster object property, in
  1090. // ascending or descending order.
  1091.  
  1092. function sortPostersBy(property, backwards)
  1093. {
  1094. posterList = [];
  1095. posterList.property = property;
  1096. for (name in posters)
  1097. posterList.push(posters[name]);
  1098. posterList.sort(comparator);
  1099.  
  1100. function comparator(a, b) {
  1101. var r;
  1102. if (a[property] > b[property])
  1103. r = 1;
  1104. else if (a[property] < b[property])
  1105. r = -1;
  1106. else if (a.sortkey > b.sortKey)
  1107. return 1;
  1108. else if (a.sortKey < b.sortKey)
  1109. return -1;
  1110. else
  1111. return 0;
  1112. if (backwards)
  1113. r = -r;
  1114. return r;
  1115. }
  1116. }
  1117.  
  1118. // -----------------------------------------------------------------------------
  1119.  
  1120. function $(id) {return document.getElementById(id);}
  1121.  
  1122. function $x(xpath, contextNode, resultType)
  1123. {
  1124. contextNode = contextNode || document.body;
  1125. resultType = resultType || XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE;
  1126. var doc = contextNode.ownerDocument; // FF3; can't just use document
  1127. return doc.evaluate(xpath, contextNode, null, resultType, null);
  1128. }
  1129.  
  1130. function $xFirst(xpath, contextNode)
  1131. {
  1132. var xpr = $x(xpath, contextNode, XPathResult.FIRST_ORDERED_NODE_TYPE);
  1133. return xpr.singleNodeValue;
  1134. }
  1135.  
  1136. // Disables a button and sets its text to the supplied string.
  1137.  
  1138. function disable(button, msg)
  1139. {
  1140. if (typeof button == 'string')
  1141. button = $(button);
  1142. if (msg)
  1143. button.value = msg;
  1144. button.disabled = true;
  1145. }
  1146.  
  1147. // Enables a button and sets its text to the supplied string.
  1148.  
  1149. function enable(button, msg)
  1150. {
  1151. if (typeof button == 'string')
  1152. button = $(button);
  1153. if (msg)
  1154. button.value = msg;
  1155. button.disabled = false;
  1156. }
  1157.  
  1158. // Adds the style rules defined in the STYLE constant to the page.
  1159.  
  1160. function addStyles()
  1161. {
  1162. var style = document.createElement('style');
  1163. style.innerHTML = STYLE;
  1164. document.getElementsByTagName('head')[0].appendChild(style);
  1165. }
  1166.  
  1167. // Makes the link to the article to be discussed go directly to the
  1168. // article without pausing and redirecting.
  1169.  
  1170. function fixArticleRedirect()
  1171. {
  1172. var link = $xFirst('.//a[starts-with(@href, "/^")]');
  1173. if (link)
  1174. link.href = link.href.replace(/^.*?\%5E/, '');
  1175. }
  1176.  
  1177. // Removes those extra blank lines that seem to crop up at the end
  1178. // of certain posts.
  1179.  
  1180. function removeBlankLines(doc)
  1181. {
  1182. var list = $x('.//br[@clear="all"]', doc);
  1183. for (var x=0, limit=list.snapshotLength; x<limit; ++x) {
  1184. var br = list.snapshotItem(x);
  1185. br.parentNode.removeChild(br);
  1186. }
  1187. }
  1188.  
  1189. // Converts posting date stamps from Pacific to local time.
  1190.  
  1191. function localizeDates(doc)
  1192. {
  1193. var list = $x('.//span[@class="date"]', doc);
  1194. for (var x=0, limit=list.snapshotLength; x<limit; ++x) {
  1195. var date = list.snapshotItem(x);
  1196. date.innerHTML = new Date(date.innerHTML).toLocaleString();
  1197. }
  1198. }
  1199.  
  1200. // Adds a Quote button next to the To link in each post having one.
  1201.  
  1202. function addQuoteButtons(doc)
  1203. {
  1204. var quoteButtonModel = document.createElement('input');
  1205. quoteButtonModel.className = 'quoteButton';
  1206. quoteButtonModel.type = 'button';
  1207. quoteButtonModel.value = 'Quote';
  1208.  
  1209. var xp = $x('.//div[@class="n2"]/a[contains(., "To ")]', doc);
  1210. for (var x=0, limit=xp.snapshotLength; x<limit; ++x) {
  1211. var link = xp.snapshotItem(x);
  1212. if (/^To \d+$/.test(link.innerHTML)) {
  1213. var quoteButton = quoteButtonModel.cloneNode(true);
  1214. quoteButton.addEventListener('mousedown', onQuotePress, false);
  1215. link.parentNode.insertBefore(quoteButton, link.nextSibling);
  1216. }
  1217. }
  1218. }
  1219.  
  1220. // Adds the Tree View and Poster Report buttons at the top of the page,
  1221. // next to the 'comments' link.
  1222.  
  1223. function addButtons()
  1224. {
  1225. var node = $xFirst('//a[contains(@href, "#comment")]');
  1226. node.innerHTML += '&nbsp;<input type=button value="View as Tree" ' +
  1227. 'id=treeButton title="Ctrl-Alt-T">';
  1228. var span = document.createElement('span');
  1229. span.innerHTML = ' <input type=button value="Poster Report" ' +
  1230. 'id=posterReport title="Ctrl-Alt-R">';
  1231. node.parentNode.appendChild(span);
  1232.  
  1233. $('treeButton').addEventListener('click', onTreeViewClick, false);
  1234. $('posterReport').addEventListener('click', onPosterReportClick, false);
  1235. }
  1236.  
  1237. // Installs a keystroke event handler to catch the keyboard shortcuts
  1238. // for the Tree View and the Poster Report.
  1239.  
  1240. function addKeys()
  1241. {
  1242. window.addEventListener('keypress', onKeyPress, false);
  1243. }
  1244.  
  1245. // Adds a blue box at the end of the page, in which to write debugging
  1246. // messages.
  1247.  
  1248. function addLogBox()
  1249. {
  1250. var div = document.createElement('div');
  1251. div.id = 'logBox';
  1252. document.body.appendChild(div);
  1253. }
  1254.  
  1255. // Writes a log message into the log box at the bottom of the page.
  1256.  
  1257. function log(msg)
  1258. {
  1259. var logBox = $('logBox');
  1260. var html = logBox.innerHTML;
  1261. if (html)
  1262. html += '<br>\n';
  1263. logBox.innerHTML = html + msg;
  1264. }
  1265.  
  1266. // Logs the time it took to perform a given task.
  1267.  
  1268. function logTime(startTime, task)
  1269. {
  1270. var time = new Date() - startTime;
  1271. var s = 'It took ' + time + ' ms to ' + task;
  1272. if (posts) {
  1273. var n = posts.length - 1;
  1274. s += ' for ' + n + ' posts; ' + (time/n).toFixed(2) +
  1275. ' ms/post';
  1276. }
  1277. log(s + '.');
  1278. }
  1279.  
  1280. // Scrolls the window to the internal anchor indicated by the 'hash'.
  1281.  
  1282. function fixLocation()
  1283. {
  1284. location.hash = location.hash;
  1285. }
  1286.  
  1287. // ------ Decrudify ------------------------------------------------------------
  1288.  
  1289. // Decrudify fixes garbage characters introduced into posts by a recent
  1290. // FR server bug in which the server breaks up the individual bytes
  1291. // of posted UTF-8 sequences into their own separate HTML entities,
  1292. // resulting in garbage characters on the screen.
  1293.  
  1294. // E.g., a left curly double-quote is unicode \u201c, which takes three
  1295. // bytes to represent in UTF-8: e2 80 9c. Instead of passing the UTF-8
  1296. // unscathed, the server substitutes the entities for small-a with a
  1297. // circumflex (e2), the euro-symbol (80), and the oe ligature (9c),
  1298. // resulting in gibberish on the screen. Decrudify finds such garbage
  1299. // sequences and substitutes the originally intended character.
  1300.  
  1301.  
  1302. // A hash containing an entry for each entity value that converts to a
  1303. // character in the range \u0080 thru \u00ff.
  1304. var charCodeToByte = {
  1305. 8364: 128,
  1306. 129: 129,
  1307. 8218: 130,
  1308. 402: 131,
  1309. 8222: 132,
  1310. 8230: 133,
  1311. 8224: 134,
  1312. 8225: 135,
  1313. 710: 136,
  1314. 8240: 137,
  1315. 352: 138,
  1316. 8249: 139,
  1317. 338: 140,
  1318. 141: 141,
  1319. 381: 142,
  1320. 143: 143,
  1321. 144: 144,
  1322. 8216: 145,
  1323. 8217: 146,
  1324. 8220: 147,
  1325. 8221: 148,
  1326. 8226: 149,
  1327. 8211: 150,
  1328. 8212: 151,
  1329. 732: 152,
  1330. 8482: 153,
  1331. 353: 154,
  1332. 8250: 155,
  1333. 339: 156,
  1334. 157: 157,
  1335. 382: 158,
  1336. 376: 159,
  1337. 160: 160,
  1338. 161: 161,
  1339. 162: 162,
  1340. 163: 163,
  1341. 164: 164,
  1342. 165: 165,
  1343. 166: 166,
  1344. 167: 167,
  1345. 168: 168,
  1346. 169: 169,
  1347. 170: 170,
  1348. 171: 171,
  1349. 172: 172,
  1350. 173: 173,
  1351. 174: 174,
  1352. 175: 175,
  1353. 176: 176,
  1354. 177: 177,
  1355. 178: 178,
  1356. 179: 179,
  1357. 180: 180,
  1358. 181: 181,
  1359. 182: 182,
  1360. 183: 183,
  1361. 184: 184,
  1362. 185: 185,
  1363. 186: 186,
  1364. 187: 187,
  1365. 188: 188,
  1366. 189: 189,
  1367. 190: 190,
  1368. 191: 191,
  1369. 192: 192,
  1370. 193: 193,
  1371. 194: 194,
  1372. 195: 195,
  1373. 196: 196,
  1374. 197: 197,
  1375. 198: 198,
  1376. 199: 199,
  1377. 200: 200,
  1378. 201: 201,
  1379. 202: 202,
  1380. 203: 203,
  1381. 204: 204,
  1382. 205: 205,
  1383. 206: 206,
  1384. 207: 207,
  1385. 208: 208,
  1386. 209: 209,
  1387. 210: 210,
  1388. 211: 211,
  1389. 212: 212,
  1390. 213: 213,
  1391. 214: 214,
  1392. 215: 215,
  1393. 216: 216,
  1394. 217: 217,
  1395. 218: 218,
  1396. 219: 219,
  1397. 220: 220,
  1398. 221: 221,
  1399. 222: 222,
  1400. 223: 223,
  1401. 224: 224,
  1402. 225: 225,
  1403. 226: 226,
  1404. 227: 227,
  1405. 228: 228,
  1406. 229: 229,
  1407. 230: 230,
  1408. 231: 231,
  1409. 232: 232,
  1410. 233: 233,
  1411. 234: 234,
  1412. 235: 235,
  1413. 236: 236,
  1414. 237: 237,
  1415. 238: 238,
  1416. 239: 239,
  1417. 240: 240,
  1418. 241: 241,
  1419. 242: 242,
  1420. 243: 243,
  1421. 244: 244,
  1422. 245: 245,
  1423. 246: 246,
  1424. 247: 247,
  1425. 248: 248,
  1426. 249: 249,
  1427. 250: 250,
  1428. 251: 251,
  1429. 252: 252,
  1430. 253: 253,
  1431. 254: 254,
  1432. 255: 255,
  1433. };
  1434.  
  1435. // Makes a regular expression that matches UTF-8 sequences
  1436. function makeUtf8Regex(table) {
  1437. table = table || charCodeToByte;
  1438. var keys = Object.getOwnPropertyNames(table).sort();
  1439. var begRange = keys[0]*1;
  1440. var prevKey = begRange;
  1441. var regExp = '[' + toUnicodeLit(192) + '-' + toUnicodeLit(247) + '][';
  1442. for (var x=1; x<keys.length; x++) {
  1443. var c = keys[x]*1;
  1444. if (c >= 192 && c <= 247)
  1445. continue;
  1446. if (c == prevKey + 1) {
  1447. prevKey++;
  1448. continue;
  1449. }
  1450. addToRegExp();
  1451. begRange = c;
  1452. prevKey = begRange;
  1453. }
  1454. addToRegExp();
  1455. regExp += ']+';
  1456. return new RegExp(regExp, 'g');
  1457.  
  1458. function addToRegExp() {
  1459. regExp += toUnicodeLit(begRange);
  1460. if (begRange < prevKey)
  1461. regExp += '-' + toUnicodeLit(prevKey);
  1462. }
  1463. }
  1464.  
  1465. // Converts a numeric character code to a unicode hex literal (\uxxxx)
  1466. function toUnicodeLit(charCode) {
  1467. var hex = ('000' + charCode.toString(16)).slice(-4);
  1468. return '\\u' + hex;
  1469. }
  1470.  
  1471. var regExp = makeUtf8Regex(charCodeToByte);
  1472.  
  1473. // Returns the input string with any UTF-8 sequences converted to JavaScript
  1474. // code points
  1475. function utf8ToString(utf8) {
  1476. fixCount++;
  1477. var p = 0;
  1478. var result = '';
  1479. while (p < utf8.length) {
  1480. var c = utf8.charCodeAt(p++);
  1481. switch (c >> 4) {
  1482. case 0:
  1483. case 1:
  1484. case 2:
  1485. case 3:
  1486. case 4:
  1487. case 5:
  1488. case 6:
  1489. case 7:
  1490. result += String.fromCharCode(c);
  1491. break;
  1492. case 8:
  1493. case 9:
  1494. case 10:
  1495. case 11:
  1496. console.log("Bad UTF-8 string: " + utf8.slice(p-16, p+16) +
  1497. ' p = ' + p);
  1498. break;
  1499. case 12:
  1500. case 13:
  1501. result += String.fromCharCode((c & 31) << 6 |
  1502. charCodeAt(utf8, p++));
  1503. break;
  1504. case 14:
  1505. result += String.fromCharCode((c & 15) << 12 |
  1506. charCodeAt(utf8, p++) << 6 |
  1507. charCodeAt(utf8, p++));
  1508. break;
  1509. case 15:
  1510. result += String.fromCodePoint((c & 7) << 18 |
  1511. charCodeAt(utf8, p++) << 12 |
  1512. charCodeAt(utf8, p++) << 6 |
  1513. charCodeAt(utf8, p++));
  1514. break;
  1515. default:
  1516. console.log("Bad charCode " + c + ' at p = ' + p + ' in ' +
  1517. utf8);
  1518. }
  1519. }
  1520. return result;
  1521. }
  1522.  
  1523. // Returns the byte value for the charCode at position x in the UTF-8 string.
  1524. // E.g., charCode 8364 (the euro-sign) converts to 128
  1525. function charCodeAt(utf8, x) {
  1526. var c = charCodeToByte[utf8.charCodeAt(x)];
  1527. if ((c >> 6) != 2)
  1528. console.log("Bad UTF8 char " + c + " at " + x + " in " + utf8);
  1529. return c & 63;
  1530. }
  1531.  
  1532. var fixCount;
  1533.  
  1534. // Walks all the text nodes in the document and decrudifies each one
  1535. function decrudify() {
  1536. var t = performance.now();
  1537. fixCount = 0;
  1538. var nodeWalker = document.createTreeWalker(document.body,
  1539. NodeFilter.SHOW_TEXT);
  1540. while (nodeWalker.nextNode())
  1541. decrudifyTextNode(nodeWalker.currentNode);
  1542. t = performance.now() - t;
  1543. var msg = "Decrudify made " + fixCount + " fix" +
  1544. (fixCount == 1 ? '' : 'es') +
  1545. ' in ' + t.toFixed(1) + "ms.";
  1546. log(msg);
  1547. console.log(msg);
  1548. }
  1549.  
  1550. // Runs utf8ToString on any UTF-8 sequences in a text node until no change
  1551. // results. Replaces the text node if any change occurred
  1552. function decrudifyTextNode(node) {
  1553. var text = node.textContent;
  1554. var originalText = text;
  1555. while (true) {
  1556. var newText = text.replace(regExp, utf8ToString);
  1557. if (newText == text)
  1558. break;
  1559. text = newText;
  1560. }
  1561. if (newText != originalText)
  1562. node.textContent = newText;
  1563. }
  1564.  
  1565. // -----------------------------------------------------------------------------
  1566.  
  1567. // Main program ...
  1568.  
  1569. var startTime = new Date();
  1570.  
  1571. addStyles();
  1572. addLogBox();
  1573. fixArticleRedirect();
  1574. decrudify();
  1575. var originalHTML = document.body.innerHTML;
  1576. removeBlankLines(document.body);
  1577. addQuoteButtons(document.body);
  1578. addButtons();
  1579. addKeys();
  1580.  
  1581. logTime(startTime, 'prepare the page');
  1582.  
  1583. // Try to correct positioning error when going to internal anchors.
  1584. if (location.hash) {
  1585. fixLocation();
  1586. document.body.addEventListener('load', fixLocation, false);
  1587. window.setTimeout(fixLocation, 500);
  1588. }