NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Storium AutoLink // @namespace db88bb4bde770bfae9f87562667c74dd // @include https://storium.com/* // @grant none // @description Turns plain text URLs etc into links. Based on code by Jesse Ruderman - http://www.squarefree.com/ and tweaked for Storium. // @version 1.2.0 // ==/UserScript== /* License: MPL, GPL, LGPL. */ /* Version Info */ // V1.0.0 First version - filters for normal URL, email and @Username /* -- */ const timeBefore = new Date(); // Fire after a delay to scoop up dynamic content //var body = document.getElementsByTagName("body")[0]; //setInterval(function(){ go(body); }, 5000); /*********************************** * Filters * ***********************************/ /* Creating new filters: Filters have three fields: * name (string) Used for tooltip on created links, e.g. "Link added by AutoLink filter: Plain Text Links". Used for class attribute of created links, e.g. "autolink autolink-plain-text-links". * regexp (regular expression) The entire text matching the regular expression will be linked. Must be global (/g). May be case-insensitive (/i). * href (function) Arguments: |match|, an output of regexp.exec. (May also treat RegExp.leftContext, etc. as inputs.) Returns: The URL to be used for a link, or |null| to cancel link creation. Must not use filter.regexp, but may use other regular expressions. This regular expression reference might be useful: http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference:Objects:RegExp If multiple filters match a string, the first filter will win. */ const filters = [ { name: "Plain text link", regexp: /https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/g, href: function(match) { return match[0]; } }, { name: "Email address", regexp: /[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig, href: function(match) { return "mailto:" + match[0]; } }, { name: "Storium User", regexp: /\@(\w+)/g, href: function(match) { return "https://storium.com/user/" + match[1]; } } ]; /*********************************** * Helper functions for filters * ***********************************/ function digits(s) { return s.replace(/[^0-9]/g, ""); } function alphanumerics(s) { return s.replace(/[^0-9a-z]/ig, ""); } /*********************************** * Link styling * ***********************************/ /* You can make links generated by AutoLink look different from normal links by editing styleLink below and/or by setting up user style sheet rules. Example: on squarefree.com, make autolinked plain text links orange. (Firefox trunk only.) @-moz-document domain(squarefree.com) { .autolink-plain-text-link { color: orange ! important; } } */ function styleLink(a, filter) { a.style.borderBottom = "1px solid orange"; } /*********************************** * Fix filters * ***********************************/ function fixFilters() { var i, r; for (i = 0; r = filters[i]; ++i) { // lowercase, and replace each run of non-alphanumerics with a single hyphen r.classNamePart = r.name.toLowerCase().replace(/[^0-9a-z]+/ig, "-"); if(!r.regexp.global) alert("AutoLink filter " + r.name + " is not global! This will break stuff!"); } } fixFilters(); /*********************************** * When and where to run * ***********************************/ var moddingDOM = false; var observer = new MutationObserver(function(mutations) { // For the sake of...observation...let's output the mutation to console to see how this all works mutations.forEach(function(mutation) { if (mutation.addedNodes.length > 0) { go(mutation.addedNodes[0]); } }); }); // Notify me of everything! var observerConfig = { attributes: false, childList: true, subtree: true, characterData: false }; window.addEventListener("load", init, false); function init() { var targetNode = document.body; observer.observe(targetNode, observerConfig); //document.addEventListener("DOMNodeInserted", nodeInserted, false); //setTimeout(go, 50, document.body); } // This makes it work at Gmail. // 20% performance penalty on a plain text file with a link on almost every line. // Tiny performance penalty on pages with few automatically added links. function nodeInserted(e) { // our own modifications should not trigger this. // (we don't want our regular expression objects getting confused) // (we want better control over when we recurse) //GM_log("Inserted: " + e.target); if (!moddingDOM) go(e.target); } /*********************************** * DOM traversal * ***********************************/ /* This script uses manual DOM traversal, in an iterative way without a stack! Advantages of snapshot XPath: * Much less code * 20-40% faster * May be possible to get another speed boost by including the regexp in the XPath expression - http://www.developer.com/xml/article.php/10929_3344421_3 * All the cool people are using it Advantages of manual DOM traversal: * Lets us stop+continue (snapshot xpath doesn't let us) * Lets us modify DOM in strange ways without worrying. * Easier to control which elements we recurse into. */ // Ignore all children of these elements. const skippedElements = { a: true, // keeps us from screwing with existing links. keeps us from recursing to death :) noscript: true, // noscript has uninterpreted, unshown text children; don't waste time+sanity there. head: true, script: true, style: true, textarea: true, label: true, select: true, button: true } const gmail = (location.host == "gmail.google.com"); function skipChildren(node) { if (node.tagName) // ! { if (skippedElements[node.tagName.toLowerCase()]) { return true; } if (gmail) { if (node.className == "ac") // gmail autocomplete (fake dropdown) return true; if (node.className == "ilc sxs") // invite foo to gmail (fake link/button) return true; } } return false; } function go(traversalRoot) { var m; // Ensure we're not already in a forbidden element. for (m = traversalRoot; m != undefined; m = m.parentNode) { if (skipChildren(m)) { return; } } // work around bug, or in case previous user scripts did crazy stuff traversalRoot.normalize(); function cont(n, didChildren) { var k = 0; // split work into chunks so Firefox doesn't freeze var q; while (n && k < 100) { ++k; // Do stuff at this node if (!didChildren && n.nodeType == 3) { if((q = runFiltersOnTextNode(n))) { n = q[0]; // if there were changes, run filters again on the new text node that's here if (q[1]) continue; } } // Traverse to the "next" node in depth-first order if (!n.firstChild) didChildren = true; if (didChildren && n == traversalRoot) break; else if (!didChildren && n.firstChild && !skipChildren(n)) { n = n.firstChild; // didChildren is already false and should stay false } else { if (n.nextSibling) { n = n.nextSibling; didChildren = false; } else { n = n.parentNode; didChildren = true; } } } // end while if (!n) { //GM_log("Odd. traversalRoot was " + traversalRoot); } else if (n == traversalRoot) { //GM_log("Done"); //alert("AutoLink time: " + (new Date() - timeBefore)) } else { // Continue after 10ms. //GM_log("will have to continue"); setTimeout(cont, 10, n, didChildren); } } // end function cont cont(traversalRoot, false); } /*********************************** * Running filters * ***********************************/ // runFiltersOnTextNode // Return: node at which to continue traversal, or |null| to mean no changes were made. function runFiltersOnTextNode(node) { // Too many variables. Good hint that I need to split this function up :P var source, j, regexp, match, lastLastIndex, k, filter, href, anyChanges; // things var used, unused, firstUnused, lastUnused, a, parent, nextSibling; // nodes source = node.data; anyChanges = false; // runFiltersOnTextNode has its own do-too-much-at-once avoider thingie. // assumption: if there is one text node with a lot of matches, // it's more important to finish quickly than be transparent. // (e.g. plain text file FULL of links) // assumption: 40 * 100 = 140. k=0; for (j = 0; filter = filters[j]; ++j) { regexp = filter.regexp; if (regexp.test(source)) { parent = node.parentNode; nextSibling = node.nextSibling; regexp.lastIndex = 0; firstUnused = null; // Optimization from the linkify that came with Greasemonkey(?): // instead of splitting a text node multiple times, take advantage // of global regexps and substring. for (match = null, lastLastIndex = 0; k < 40 && (match = regexp.exec(source)); ) { // this should happen first, so RegExp.foo is still good :) href = genLink(filter, match); if (href != null && href != location.href) { ++k; unused = document.createTextNode(source.substring(lastLastIndex, match.index)); if (!anyChanges) { anyChanges = true; parent.removeChild(node); firstUnused = unused; moddingDOM = true; } parent.insertBefore(unused, nextSibling); used = document.createTextNode(match[0]) a = document.createElement("a"); a.href = href; a.title = "Link added by AutoLink filter: " + filter.name; a.className = "autolink autolink-" + filter.classNamePart; styleLink(a, filter); a.appendChild(used); parent.insertBefore(a, nextSibling); lastLastIndex = regexp.lastIndex; } } if (anyChanges) { lastUnused = document.createTextNode(source.substring(lastLastIndex)); parent.insertBefore(lastUnused, nextSibling); moddingDOM = false; return [firstUnused, true] } return [node, false]; } } return null; } function genLink(filter, match) { try { return filter.href(match); } catch(er) { return "data:text/plain,Error running AutoLink function for filter: " + encodeURIComponent(filter.name) + "%0A%0A" + encodeURIComponent(er); } }