/* * !!! Please read this before modifying !!! * * One of the public benefits of this extension is that everyone's quotes look the * same -- Fark threads are more visually consistant and look less messy. Please think * twice before customizing this code because you want a different format (or color, or * whatever). The current output is based on the defacto-standard that was being widely * used before I even wrote Farkit. */ window.addEventListener("load", function() { FARKIT.init(); }, false); // Extension code stored in a single object. This elimiates problems with namespace collisions. var FARKIT = { _logService : null, _prefs : null, _menuitem1 : null, _menuitem2 : null, _key1 : null, _key2 : null, _onFark : false, _onFarkStart : null, // // log() // // Log debug messages to the Javascript console. // Note: javascript.options.showInConsole must be enabled // log : function (message) { if (!FARKIT._prefs.getBoolPref("debug")) { return; } if (this._logService == null) { this._logService = Components.classes['@mozilla.org/consoleservice;1'].getService(); this._logService.QueryInterface(Components.interfaces.nsIConsoleService); } this._logService.logStringMessage("Farkit: " + message); }, // // _locationListener // // Receive updates whenever the location bar changes. This happens when a tab // is switched or the user navigates to another page. It does NOT fire for // tabs in the background. // _locationListener : { QueryInterface : function (aIID) { if (aIID.equals(Components.interfaces.nsIWebProgressListener) || aIID.equals(Components.interfaces.nsISupportsWeakReference) || aIID.equals(Components.interfaces.nsISupports)) return this; throw Components.results.NS_NOINTERFACE; }, onLocationChange : function (aProgress, aRequest, aURI) { // try/catch needed when URI has no host (eg about:blank). Even a // "typeof(aURI.host) == undefined" check throws an error. [XXX - OK in FF2] var onFark = false, onFarkForum = false; try { if (aURI.host) { switch (aURI.host) { case "forums.fark.com": onFarkForum = true; /* FALLTHRU */ case "fark.com": case "totalfark.com": case "www.fark.com": case "www.totalfark.com": onFark = true; break; case "cgi.fark.com": // alternate view if (/^[^\?]*\/comments.pl/.test(aURI.path)) { onFarkForum = true; } onFark = true; break; default: break; } } } catch (e) {} FARKIT.updateMenuForLocation(onFarkForum); // Compute time spent on Fark function savetime() { // Increment the counter var farkTime = Math.round(new Date().getTime() / 1000) - FARKIT._onFarkStart; var farkTimeTotal = FARKIT._prefs.getIntPref("farkTime"); farkTimeTotal += farkTime; FARKIT._prefs.setIntPref("farkTime", farkTimeTotal); } if (onFark && !FARKIT._onFark) { // We have changed state to on-fark FARKIT._onFark = true; FARKIT._onFarkStart = Math.round(new Date().getTime() / 1000); } else if (!onFark && FARKIT._onFark) { // We have changed state to off-fark savetime(); FARKIT._onFark = false; FARKIT._onFarkStart = null; } else if (onFark && FARKIT._onFark) { // We are still on a Fark page. Save the time anyway. savetime(); FARKIT._onFarkStart = Math.round(new Date().getTime() / 1000); } return 0; }, // unused stubs onStateChange: function() { return 0; }, onProgressChange: function() { return 0; }, onStatusChange: function() { return 0; }, onSecurityChange: function() { return 0; }, onLinkIconAvailable: function() { return 0; } }, // // init() // // Called once, when browser window opens. // init: function() { FARKIT._prefs = Components.classes["@mozilla.org/preferences-service;1"].getService( Components.interfaces.nsIPrefService).getBranch("extensions.farkit."); // Open JS Console when debugging. [This function is what the FF menu calls] if (FARKIT._prefs.getBoolPref("debug")) { toJavaScriptConsole(); } FARKIT.log("Initializing..."); // Locate XUL menu entry this._menuitem1 = document.getElementById("farkit-menu-entry1"); this._menuitem2 = document.getElementById("farkit-menu-entry2"); // Locate XUL key shortcuts this._key1 = document.getElementById("farkit-key1"); this._key2 = document.getElementById("farkit-key2"); // Attach a lister for location changes gBrowser.addProgressListener(this._locationListener, Components.interfaces.nsIWebProgress.NOTIFY_STATE_DOCUMENT); // Attach a listener to the right-click context menu var menu = document.getElementById("contentAreaContextMenu"); menu.addEventListener("popupshowing", this.updateMenuForPopup, false); FARKIT.log("Initialization done."); }, // // updateMenuForLocation() and updateMenuForPopup() // // Together, these two callbacks manage the context (right-click) popup menu // and keyboard shortcuts. One is called when the page location changes, so that // the entries are only visible on Fark forum pages. The other is called when // the context menu is displayed, to enable/disable entries based on the current // state of things. // updateMenuForLocation : function (enabled) { FARKIT._menuitem1.hidden = !enabled; FARKIT._menuitem2.hidden = !enabled; FARKIT._key1.setAttribute("disabled", !enabled); FARKIT._key2.setAttribute("disabled", !enabled); }, updateMenuForPopup : function (event) { // We'll assume that if the Farkit menuentry1 isn't visible, we're not on Fark. if (!FARKIT._menuitem1.hidden) { // Disable quoting unless some text is selected. [Always false if in comment box.] var noSelection = !gContextMenu.isTextSelected FARKIT._menuitem1.setAttribute("disabled", noSelection); // Hide pasting unless we're in the comment box. Disable it if there's no URL. var HTMLnode = document.popupNode; var wrongPlace = (HTMLnode.nodeName != "TEXTAREA" || HTMLnode.name != "comment"); FARKIT._menuitem2.hidden = wrongPlace; FARKIT._menuitem2.setAttribute("disabled", (FARKIT.getClipboardURL() == null)); } }, // // doQuoteorPaste() // // Called by the key handler. If we're currently inside a text area, do // a Farkit Paste. Otherwise do the normal user quoting. // doQuoteOrPaste : function (trigger, event) { var ele = document.commandDispatcher.focusedElement; if (ele && ele.nodeName == 'TEXTAREA') { FARKIT.doPaste(trigger, event); } else { FARKIT.doQuote(trigger, event); } }, /////////////////////////////////////////////////////////////////////////////// // // doQuote() // // Primary Farkit function: quote the selected text into the comment box. // doQuote : function (trigger, event) { var ok; var commentbox; var quote; var lastUsername, username; var prefix = ""; var scroll = true; FARKIT.log("Quoting activated from " + trigger + ", Shift-key? " + event.shiftKey); // Check to see if we're quoting in no-scroll mode switch (trigger) { case "menu" : // can't use .altKey; modifier prevents menu from popping up, or dismisses it. if (event.shiftKey == false) { break; } /* fallthru */ case "key-alt" : scroll = false; break; case "key" : default : /* fallthru */ } // First, grab the selected text (formatted). quote = FARKIT.getSelectedText(); if (quote == null) { return; } // errors alerted in call // Find the comment box for this thread (and make sure HTML is enabled). commentbox = FARKIT.getCommentBox(); if (commentbox == null) { return; } // errors alerted in call // Figure out who is being quoted username = FARKIT.getSelectionUsername(); if (username == null) { return; } // errors alerted in call // Recall who (if anyone) we last quoted for this comment. // This is an easy way to store semi-persistant that's unique per tab and destroyed // when the page changes (or is reloaded, or revisited with the Back button). lastUsername = commentbox.getAttribute("x-FarkIt-lastUsername"); // Determine if the quote should be prefixed with an attribution. // We don't handle edits by the user, but we can check for a completely empty box. if (lastUsername != username || commentbox.value.length == 0) { commentbox.setAttribute("x-FarkIt-lastUsername", username); prefix = "" + username + ": "; } if (commentbox.value.length != 0) { // If this is the first quote, notify the user there is already text in the comment. // Firefox saves the previous comment when clicking "Back" (after submitting it), this // helps ensure old comments don't get accidently posted again with the new comment. if (lastUsername == null) { ok = confirm("Farkit: There is already text in your comment box. Append the quote?"); if (!ok) { return; } } // Ensure there is a blank line following the existing comment text. prefix = FARKIT.getCommentSpacing(commentbox) + prefix; } // Put the prefix (if any) and the quote into the comment box. commentbox.value = commentbox.value + prefix + quote + "\n\n"; if (scroll) { // scroll to commentbox, also deselects the quoted text commentbox.focus(); // scroll a bit more so submit button is visible // Could find the button and scroll to it directly, but this is good enough. content.window.scrollByLines(2); } else { // Toss in an XXX marker so it's obvious where to type a reply. commentbox.value += "XXX"; // Remove the selection hilighting to indicate success. content.window.getSelection().removeAllRanges(); } // Scroll commentbox to bottom (list "page") so the new quote is visible. commentbox.scrollTop = commentbox.scrollHeight - commentbox.clientHeight; // Increment the quote counter var numQuotes = FARKIT._prefs.getIntPref("numQuotes"); numQuotes++; FARKIT._prefs.setIntPref("numQuotes", numQuotes); }, /////////////////////////////////////////////////////////////////////////////// // // doPaste() // // Secondary Farkit function: paste formatted URL from clipboard into comment box // doPaste : function(trigger, event) { FARKIT.log("Pasting activated from " + trigger); var url = FARKIT.getClipboardURL(); if (!url) { alert("Farkit: No URL in clipboard to paste."); return; } var isImage = /\.(gif|jpg|jpeg|jpe|png)$/i.test(url); // Find the comment box for this thread (and make sure HTML is enabled). var commentbox = FARKIT.getCommentBox(); if (commentbox == null) { return; } // errors alerted in call // Extract the selected text (if any). var selStart = commentbox.selectionStart; var selEnd = commentbox.selectionEnd; var body = commentbox.value.substring(selStart, selEnd); // If it's an image, but text is selected, treat is as a link instead. if (isImage && body.length > 0) { isImage = false; } // Format the tag(s) we're inserting var header, footer; if (isImage) { header = ''; body = ''; footer = ''; } else { header = ''; footer = ' (pops)'; } // Toss the tag text into the comment box. // Using a command is efficient, and adds this edit to the undo stack. try { var command = "cmd_insertText"; var controller = document.commandDispatcher.getControllerForCommand(command); if (!controller) { throw("can't get controller"); } if (!controller.isCommandEnabled(command)) { throw("controller is disabled!"); } controller = controller.QueryInterface(Components.interfaces.nsICommandController); if (!controller) { throw("controller QI failed"); } var params = Components.classes["@mozilla.org/embedcomp/command-params;1"]; if (!params) { throw("can't get params component"); } params = params.createInstance(Components.interfaces.nsICommandParams); if (!params) { throw("can't create params instance"); } params.setStringValue("state_data", header + body + footer); controller.doCommandWithParams(command, params); } catch (e) { alert("Farkit: Internal error pasting into comment box: " + e); } // Increment the paste counter var numPastes = FARKIT._prefs.getIntPref("numPastes"); numPastes++; FARKIT._prefs.setIntPref("numPastes", numPastes); return; }, // // getSelectedText() // // Returns the cleaned and formatted text which was selected by the user. // getSelectedText : function() { var selection = new String(content.window.getSelection()); // ??? try to fix ghostly newline from "foo\nfoo" selection = selection.replace(/\r\n/g, "\n"); // strip leading and trailing whitespace selection = selection.replace(/^\s*/, ''); selection = selection.replace(/\s*$/, ''); // strip extra whitespace between paragraphs. selection = selection.replace(/\s*\n\s*\n\s*/g, "\n\n"); // strip multiple blank lines selection = selection.replace(/\n{3,}/g, "\n\n"); // (at this point, text should only have mix of "item\nitem" and "para\n\npara") // Check for empty selection if (selection.length == 0) { alert("Farkit: You need to select some text to quote!"); return null; } // Mumble hummm mumble... if (FARKIT._prefs.getBoolPref("loremipsum")) { selection = FARKIT.loremize(selection); } // Check for huge quotes in case of accidental select-all // XXX - might want to have a newline-count check too. if (selection.length > 1700) { ok = confirm("Farkit: You are quoting a large amount of text. Are you sure you need to?"); if (!ok) { return null; } } // Make sure any plain text isn't accidently intrepreted as HTML by Fark selection = selection.replace(/&/g, "&"); selection = selection.replace(//g, ">"); // Put quote in italics. We do this to each paragraph, so that it's // easier to reply to each one (without manually entering HTML). selection = selection.replace(/(\n+)/g, "$1"); selection = "" + selection + ""; return selection; }, // // getClipboardURL() // // Scans for an URL in the system clipboard and returns it. // getClipboardURL : function() { // Prepare to grab text from the clipboard var clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]. createInstance(Components.interfaces.nsIClipboard); var clipboardXfer = Components.classes["@mozilla.org/widget/transferable;1"]. createInstance(Components.interfaces.nsITransferable); if (!clipboard || !clipboardXfer) { FARKIT.log("Error: can't get system clipboard or transfer component(s)"); return null; } clipboardXfer.addDataFlavor("text/unicode"); clipboard.getData(clipboardXfer, clipboard.kGlobalClipboard); // Grab the text from the clipboard var clipboardText = ""; try { var uniData = new Object(); var uniDataLen = new Object(); clipboardXfer.getTransferData("text/unicode", uniData, uniDataLen); if (!uniData) { throw("can't get transfer data"); } uniData = uniData.value.QueryInterface(Components.interfaces.nsISupportsString); if (!uniData) { throw("QI on uniData failed"); } clipboardText = uniData.data.substring(0, uniDataLen.value / 2); } catch (e) { // Normal: if clipboard is empty, getTransferData() will throw. // alert("Farkit: Internal error getting text from clipboard: " + e); FARKIT.log("no text found in clipboard (throw from getTransferData is normal): " + e); return null; } // Did we get anything? if (clipboardText.length <= 0) { // This case is probably always handled in the catch() above. FARKIT.log("can't paste, there is nothing in clipboard."); return null; } // Check if text is completely an URL [Verified that Fark supports HTTP, HTTPS, and FTP links.] var matches = /^((?:http:|https:|ftp:)\/\/\S+)$/i.exec(clipboardText); if (!matches) { // Look for an URL embedded in the text. // XXX - what if there are 2 or more matches? // XXX - may need allow additional ending characters, but we're trying to strip trailing punctuation. matches = /((?:http:|https:|ftp:)\/\/\S+[\w\/\\\-])/i.exec(clipboardText); if (!matches) { FARKIT.log("Can't paste, no URLs found in clipboard."); return null; } } return matches[1]; }, // // getCommentBox() // // Finds the Fark comment box in the page's HTML, and checks to see that // the user's HTML privledges are enabled. // getCommentBox : function() { var nodes = content.document.getElementsByName("comment"); if (nodes.length != 1) { alert("Farkit: Can't quote, no comment box found for this thread."); return null; } var commentbox = nodes[0]; nodes = content.document.getElementsByName("use_html"); if (nodes.length != 1) { alert("Farkit: 'HTML Enabled' not available for thread, or your HTML privledges were revoked."); return null; } var htmlCheckbox = nodes[0]; if (!htmlCheckbox.checked) { alert("Farkit: You must check the 'HTML Enabled' option below the comment box to quote a user."); return null; } return commentbox; }, // // getSelectionUsername() // // Figure out what username to associate with the selected text. Also performs checks for // illegal selections (non-comment, non-headline, multiple comments, etc.). // getSelectionUsername : function() { // Note that getSelection() actually returns a Selection object, not text (but .toString() is there). // // Selections can traverse multiple nodes: .anchorNode is related to where the click+drag began, // and .focusNode is related to where it ended. We'll work with the Range object returned by // selection.getRangeAt(0), because it won't flip the start/end depending on the direction // of the selection. // // If an endpoint was in a #text node, the offset for the endpoint is within the text length. But if // the endpoint is at element node (eg BR/IMG), then the endpoint node is the *parent*, and the // offset is to which child. // // EG: "

foo bar baz
// Select from "bar baz" to the line break below it. The startNode will be the #text node with an // offset of 4 ("foo "), while the endNode is the DIV node, with an offset of 2 (because the BR which // got selected is the 3rd child of the DIV). // // On Fark, an endpoint node is usually a #text node within the DIV/ctext (if an image is selected, the // node may actually be the DIV itself). So we'll first move up the tree, looking for the DIV. It's // also possible the user selected the BR's offsetting the comment text, in which case we should look // sideways (nextNode/prevNode) for the DIV. [This doesn't need to loop, because Fark's current HTML // has all the valid selection nodes sequential. // // range.commonAncestorContainer might be a useful shortcut [we should either get the DIV (or child), // or the middle column TD]. The TD isn't readily identifiable, though. var username; var selRange = content.window.getSelection().getRangeAt(0); var selStart = selRange.startContainer; var selEnd = selRange.endContainer; FARKIT.log("startNode: " + selStart.nodeName + " endNode: " + selEnd.nodeName); FARKIT.log("startOffset: " + selRange.startOffset + " endOffset: " + selRange.endOffset); var headlineStart = FARKIT.isHeadline(selStart); var headlineEnd = FARKIT.isHeadline(selEnd); // If either end is in the headline, process it as a headline selection. if (headlineStart || headlineEnd) { if (headlineStart && headlineEnd) { username = "submitter"; } else { alert("Farkit error: Your headline selection includes non-headline text."); return null; } } else { // Find the nearest comment section (DIV). selStart = FARKIT.getNearestComment(selStart, selRange.startOffset, true); selEnd = FARKIT.getNearestComment(selEnd, selRange.endOffset, false); if (selStart == null || selEnd == null) { alert("Farkit Error: Your selected text includes part of something that isn't a comment or headline."); return null; } if (selStart != selEnd) { alert("Farkit Error: You have selected text from two or more users. One at a time, please."); return null; } username = FARKIT.getUsernameByNode(selStart); if (username == null) { alert("Farkit Error: Can't determine user being quoted. Fark's HTML may have changed."); return null; } } return username; }, // // isHeadline() // // Checks to see if the given node is in the headline. // isHeadline : function (node) { var result = false; while (node) { if (node.nodeName == "TD" && node.className == "nilink") { result = true; break; } node = node.parentNode; } return result; }, // // getNearestComment() // // Finds the nearest comment (DIV) to the given node. We will search up the DOM tree until we find // it (or fall off). We also look a little bit sideways in some cases. Note that the search will // delibrately fail if the selection isn't in an expected area. We don't want any DIV in that case. // // Normally, the comment DIVs are all children of a TD holding the entire middle of the page. But // if the thread has voting enabled, the DIVs are all children of a FORM. // getNearestComment : function (node, offset, start) { if (node.nodeName == "TD" || (node.nodeName == "FORM" && node.action == "http://cgi.fark.com/cgi/fark/comments-vote.pl")) { // This test shouldn't be needed, as it should always be true. But selecting text in a // headline into the whitespace by the Fark tag was causing an offset of 1 in a single-child // TD node. I don't quote understand why. // if (offset >= node.childNodes.length) { offset = node.childNodes.length - 1; } node = node.childNodes[offset]; // If we're not already there, walk through siblings until we find a comment. if (node.nodeName != "DIV" || node.className != "ctext") { if (start) { node = node.nextSibling; } else { node = node.previousSibling; } // Check the sibling if (node == null || node.nodeName != "DIV" || node.className != "ctext") { node = null; } } } else { while (node) { // Have we walked up to a comment DIV? if (node.nodeName == "DIV" && node.className == "ctext") { break; } // Check for TABLE/ctable here if we ever want to handle selecting the header. node = node.parentNode; } } return node; }, // // getCommentSpacing() // // Determine how many newlines should be added to the comment box to ensure proper spacing // between quotes (at least one blank line). Returns 0, 1, or 2 newlines. // getCommentSpacing : function (commentbox) { if (/\n\n$/.test(commentbox.value)) { return ""; } if (/\n$/.test(commentbox.value)) { return "\n"; } return "\n\n"; }, loremdata : [ ["a", "e", "u"], ["ac", "ad", "at", "et", "eu", "ex", "id", "in", "mi", "no", "te", "ut"], ["cum", "dis", "dui", "est", "hac", "leo", "mus", "nam", "nec", "non", "per", "sed", "sem", "sit", "vel"], ["amet", "ante", "arcu", "cras", "diam", "duis", "eget", "elit", "enim", "erat", "eros", "nibh", "nisi", "nisl", "nunc", "odio", "orci", "pede", "quam", "quis", "urna"], ["augue", "class", "curae", "dolor", "donec", "etiam", "fames", "felis", "fusce", "ipsum", "justo", "lacus", "lorem", "magna", "massa", "metus", "morbi", "neque", "netus", "nulla", "porta", "proin", "purus", "risus", "velit", "vitae"], ["aenean", "aptent", "auctor", "congue", "cursus", "dictum", "lectus", "libero", "ligula", "litora", "luctus", "magnis", "mattis", "mauris", "mollis", "montes", "nostra", "nullam", "ornare", "platea", "primis", "rutrum", "sapien", "semper", "sociis", "taciti", "tellus", "tempor", "tempus", "tortor", "turpis", "varius"], ["aliquam", "aliquet", "blandit", "commodo", "conubia", "cubilia", "dapibus", "egestas", "euismod", "feugiat", "gravida", "iaculis", "integer", "lacinia", "laoreet", "natoque", "nonummy", "posuere", "potenti", "pretium", "quisque", "rhoncus", "sodales", "vivamus", "viverra"], ["accumsan", "bibendum", "dictumst", "eleifend", "facilisi", "faucibus", "habitant", "inceptos", "interdum", "lobortis", "maecenas", "molestie", "nascetur", "pharetra", "placerat", "praesent", "pulvinar", "sagittis", "senectus", "sociosqu", "suscipit", "torquent", "ultrices", "vehicula", "volutpat"], ["consequat", "convallis", "curabitur", "dignissim", "elementum", "facilisis", "fermentum", "fringilla", "habitasse", "hendrerit", "hymenaeos", "imperdiet", "malesuada", "penatibus", "phasellus", "porttitor", "ridiculus", "tincidunt", "tristique", "ultricies", "venenatis", "vulputate"], ["adipiscing", "consetetur", "parturient", "sadipscing", "vestibulum"], ["condimentum", "scelerisque", "suspendisse", "ullamcorper"], ["consectetuer", "pellentesque", "sollicitudin"], ], // // loremize() // // Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt // ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco // laboris nisi ut aliquip ex ea commodo consequat. // loremize : function (text) { var words, newword, oldword, newtext = ""; // Grab any non-word stuff at the beginning. var results = /^([^A-Za-z]+)/.exec(text); if (results) { newtext += results[1]; } // Each loop, grab a word and the non-word characters trailing it. do { results = /([a-z]+)(?:'[a-z]{1,2}|\-)?([^a-z]*)/ig.exec(text); if (!results) { continue; } // convert the word oldword = results[1]; words = FARKIT.loremdata[oldword.length - 1] if (words) { // select a random word newword = words[Math.floor(words.length * Math.random())]; // capitalize any characters that were capitalized in the old word for (var i = 0; i < newword.length; i++) { if (oldword.charAt(i) == oldword.charAt(i).toUpperCase()) { newtext += newword.charAt(i).toUpperCase(); } else { newtext += newword.charAt(i); } } } else { newtext += oldword; } // append any non-word characters we found. newtext += results[2]; } while (results); return newtext; }, // // getUsernameByNode() // // Given a comment node (or a child thereof), return who wrote the comment. // getUsernameByNode : function (node) { var nodes, username = null; // Because node is a result from a previous getNearestComment call, we know // that it's already the class="ctext" DIV. No need to step thru parent nodes. // The comment header should be a previous sibling. while ((node = node.previousSibling) != null) { if (node.nodeName == "TABLE" && node.className && (node.className == "ctableTF" || node.className == "ctable")) { break; } } if (node == null) { return null; } // alternate view try { if (node.rows[0].cells[0].className == 'main') { node = node.previousSibling.previousSibling; } } catch (e) { } // (Farky code to handle different HTML in voting results thread. Not needed for Farkit, as there's no commentbox.) // //if (node.previousSibling && node.previousSibling.previousSibling) { // var otherHeader = node.previousSibling.previousSibling; // if (otherHeader.className && otherHeader.className == "ctable") { node = otherHeader; } //} // node is now the TABLE/class=ctable (comment header). Grab the cells in the first row. nodes = node.rows[0].cells; // Find the TD/class=clogin cell for (var i = 0; i < nodes.length; i++) { if (nodes[i].className == "clogin") { // Grab the TD -> A -> text directly //username = new String(nodes[i].firstChild.firstChild.nodeValue); username = nodes[i].getElementsByTagName('a')[0].innerHTML; break; } } return username; } };