// drupal replayer. // by M.W.Park 2008-06-03 // // main features: // 1. shows articles by time. // 2. shows new article(s) quickly. // // key bindings: // n - next // p - prev // , - top // . - bottom // (if a key above pressed with shift, navigates NEW article(s) only). // k - tracker page // // some code refs: // mozilla example (http://developer.mozilla.org/en/docs/Whitespace_in_the_DOM) for node manipulation. // me2DAY Shortcut 0.5 (http://jeongsw.springnote.com/pages/1227312) - for keydown event handler. // // ==UserScript== // @name drupal-replayer // @namespace http://kldp.org/ // @description replays drupal comments for readablity // @include http://kldp.org/node/* // @exclude // ==/UserScript== // some code import from the document http://developer.mozilla.org/en/docs/Whitespace_in_the_DOM /** * Throughout, whitespace is defined as one of the characters * "\t" TAB \u0009 * "\n" LF \u000A * "\r" CR \u000D * " " SPC \u0020 * * This does not use Javascript's "\s" because that includes non-breaking * spaces (and also some other characters). */ /** * Determine whether a node's text content is entirely whitespace. * * @param nod A node implementing the |CharacterData| interface (i.e., * a |Text|, |Comment|, or |CDATASection| node * @return True if all of the text content of |nod| is whitespace, * otherwise false. */ function is_all_ws( nod ) { // Use ECMA-262 Edition 3 String and RegExp features return !(/[^\t\n\r ]/.test(nod.data)); } /** * Determine if a node should be ignored by the iterator functions. * * @param nod An object implementing the DOM1 |Node| interface. * @return true if the node is: * 1) A |Text| node that is all whitespace * 2) A |Comment| node * and otherwise false. */ function is_ignorable( nod ) { return ( nod.nodeType == 8) || // A comment node ( (nod.nodeType == 3) && is_all_ws(nod) ); // a text node, all ws } /** * Version of |previousSibling| that skips nodes that are entirely * whitespace or comments. (Normally |previousSibling| is a property * of all DOM nodes that gives the sibling node, the node that is * a child of the same parent, that occurs immediately before the * reference node.) * * @param sib The reference node. * @return Either: * 1) The closest previous sibling to |sib| that is not * ignorable according to |is_ignorable|, or * 2) null if no such node exists. */ function node_before( sib ) { while ((sib = sib.previousSibling)) { if (!is_ignorable(sib)) return sib; } return null; } /** * Version of |nextSibling| that skips nodes that are entirely * whitespace or comments. * * @param sib The reference node. * @return Either: * 1) The closest next sibling to |sib| that is not * ignorable according to |is_ignorable|, or * 2) null if no such node exists. */ function node_after( sib ) { while ((sib = sib.nextSibling)) { if (!is_ignorable(sib)) return sib; } return null; } /** * Version of |lastChild| that skips nodes that are entirely * whitespace or comments. (Normally |lastChild| is a property * of all DOM nodes that gives the last of the nodes contained * directly in the reference node.) * * @param sib The reference node. * @return Either: * 1) The last child of |sib| that is not * ignorable according to |is_ignorable|, or * 2) null if no such node exists. */ function last_child( par ) { var res=par.lastChild; while (res) { if (!is_ignorable(res)) return res; res = res.previousSibling; } return null; } /** * Version of |firstChild| that skips nodes that are entirely * whitespace and comments. * * @param sib The reference node. * @return Either: * 1) The first child of |sib| that is not * ignorable according to |is_ignorable|, or * 2) null if no such node exists. */ function first_child( par ) { var res=par.firstChild; while (res) { if (!is_ignorable(res)) return res; res = res.nextSibling; } return null; } /** * Version of |data| that doesn't include whitespace at the beginning * and end and normalizes all whitespace to a single space. (Normally * |data| is a property of text nodes that gives the text of the node.) * * @param txt The text node whose data should be returned * @return A string giving the contents of the text node with * whitespace collapsed. */ function data_of( txt ) { var data = txt.data; // Use ECMA-262 Edition 3 String and RegExp features data = data.replace(/[\t\n\r ]+/g, " "); if (data.charAt(0) == " ") data = data.substring(1, data.length); if (data.charAt(data.length - 1) == " ") data = data.substring(0, data.length - 1); return data; } // END of import (from the document http://developer.mozilla.org/en/docs/Whitespace_in_the_DOM) Array.find = function(a, element){ for(var i = 0; i < a.length; i++){ if(a[i] == element){ return i; } } return -1; } var comments = document.evaluate('//div[@class=\"comment\"]', document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); var newComment = document.evaluate('//a[@id=\"new\"]', document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); // sets page URL. var URL = document.URL; if (URL.split('#').length > 1) URL = URL.split('#')[0]; var pos = -1; var ids = new Array(); var newPos = -1; var NEW_HEAD = -1; if (comments.snapshotLength > 0) { for (var i = 0; i < comments.snapshotLength; i++) { var node = comments.snapshotItem(i); var a = node_before(node); if (a) { var id = a.getAttribute("id"); ids[i] = id; } else { alert(i + " is null"); } } ids.sort(function (a, b) { aNum = a.split('-')[1]; bNum = b.split('-')[1]; return aNum - bNum; }); //alert(ids); } if (newComment.snapshotLength > 0) { // should be 0 or 1. var base = newComment.snapshotItem(0); var a = node_after(base); var newId = a.getAttribute("id"); var i = Array.find(ids, newId); NEW_HEAD = i; newPos = i - 1; if (document.URL.split('#').length > 1 && "new" == document.URL.split('#')[1]) { // clicked 'new'? pos = i; } else { pos = i - 1; } } function previous() { if (pos > 0) { pos--; window.location.href = URL + "#" + ids[pos]; } else { top(); } } function next() { if (pos < ids.length -1) { pos++; window.location.href = URL + "#" + ids[pos]; } else { bottom(); } } function top() { pos = -1; window.location.href = URL; } // NOTE: not the physical bottom, just the last comment. function bottom() { pos = ids.length -1; window.location.href = URL + "#" + ids[pos]; } function newPrevious() { if (newPos > NEW_HEAD) { newPos--; window.location.href = URL + "#" + ids[newPos]; } else { newTop(); } } function newNext() { if (newPos < ids.length -1) { newPos++; window.location.href = URL + "#" + ids[newPos]; } else { newBottom(); } } function newTop() { newPos = NEW_HEAD; window.location.href = URL + "#" + ids[newPos]; } // NOTE: not the physical bottom, just the last comment. function newBottom() { newPos = ids.length -1; window.location.href = URL + "#" + ids[newPos]; } // registers key bindings (emacs-like binding). // shortcuts didn't work with GM_registerMenuCommand. only menu selection. // ref: http://greasemonkey.mozdev.org/authoring.html function registerKeyBindings() { GM_registerMenuCommand("previous", previous, "p", "shift", "p"); GM_registerMenuCommand("next", next, "n", "shift", "n"); GM_registerMenuCommand("top", top, ",", "control shift", ","); GM_registerMenuCommand("bottom", bottom, ".", "control shift", "."); } registerKeyBindings(); // keydown event handler. function shortcut(event) { var target = event.target || event.srcElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') { return; } if (!event.altKey && !event.ctrlKey) { var invokeNew = false; if (NEW_HEAD > -1 && event.shiftKey) invokeNew = true; switch (event.keyCode) { case 80: // P if (invokeNew) newPrevious(); else previous(); break; case 78: // N if (invokeNew) newNext(); else next(); break; case 188: // , if (invokeNew) newTop(); else top(); break; case 190: // . if (invokeNew) newBottom(); else bottom(); break; case 75: // K window.location.href = "http://kldp.org/ktracker2" break; } } } document.addEventListener('keydown', shortcut, false);