NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Make Bookmarklets from Javascript URLs // @namespace MBFU // @description When it sees a link to a userscript or general Javascript URL, adds a Bookmarklet besides it, which you can drag to your toolbar to load the script when you next need it (running outside Greasemonkey of course). // @include http://hwi.ath.cx/*/gm_scripts/* // @include http://hwi.ath.cx/*/userscripts/* // @include http://*userscripts.org/* // @include https://*userscripts.org/* // @include http://openuserjs.org/* // @include https://openuserjs.org/* // @include http://greasyfork.org/* // @include https://greasyfork.org/* // @include http://*/* // @include https://*/* // @exclude http://hwi.ath.cx/code/other/gm_scripts/joeys_userscripts_and_bookmarklets_overview.html // @exclude http://neuralyte.org/~joey/gm_scripts/joeys_userscripts_and_bookmarklets_overview.html // @grant none // @version 1.2.5 // ==/UserScript== // BUG: We had (i%32) in a userscript (DLT) but when this was turned into a bookmarklet and dragged into Chrome, the debugger showed it had become (i2), causing the script to error with "i2 is not defined". Changing the code to (i % 32) worked around the problem. // TODO: All links with using dummy=random should change the value on mouseover/mousemove/click, so they can be re-used live without refreshing the page. // Static bookmarklets could timeout and re-build after ... 10 seconds? ^^ // Most people will NOT want the NoCache version. It's only useful for script developers. // Firefox/Greasemonkey does not like to install scripts ending ".user.js?dummy=123" // But in Chrome it is a useful way to prevent the script from being cached when you are developing it. var inGoogleChrome = window && window.navigator && window.navigator.vendor.match(/Google/); // It doesn't make much sense for me to make it browser-dependent, because I share my bookmarks between Chrome and Firefox (using XMarks). var preventBrowserFromCachingBookmarklets = inGoogleChrome; //var preventBrowserFromCachingBookmarklets = true; // Sometimes Chrome refuses to acknowledge that a script has been updated, and repeatedly installs an old version from its cache! var preventCachingOfInstallScripts = inGoogleChrome; // BUG: Chrome sometimes installs a new instance of an extension for each click, rather than overwriting (upgrading) the old. However disabling preventBrowserFromCachingBookmarklets does not fix that. It may have been the name "Wikimedia+"? var addGreasemonkeyLibToBookmarklets = true; // DONE: All bookmarklets optionally preload the Fallback GMAPI. // DONE: All bookmarklets optionally load in non-caching fashion (for changing scripts). // DONE: Use onload event to ensure prerequisite scripts are loaded before dependent scripts. // DONE via FBGMAPI: We could provide neat catches for GM_ API commands so they won't fail entirely. // TODO: Provide extra feature, which allows the Bookmarks to actually trigger // the userscript properly-running inside Greasemonkey, if this userscript is // present to handle the request, otherwise (with warning) load outside GM. // TODO: Support @include/@excude and @require meta rules? // This requires parsing the script's header using XHR before creating each bookmarklet. // DONE: Optionally create static bookmarklet, with all code inline, rather than loaded from a URL. // // comments will need to be removed or converted to /*...*/ comments var addDateToStaticBookmarklets = true; var defaultScripts = []; var includeGMCompat = addGreasemonkeyLibToBookmarklets; if (includeGMCompat) { // defaultScripts.push("http://hwi.ath.cx/code/other/gm_scripts/fallbackgmapi/fallbackgmapi.user.js"); defaultScripts.push("//neuralyte.org/~joey/gm_scripts/fallbackgmapi/fallbackgmapi.user.js"); } function buildLiveBookmarklet(link) { var neverCache = preventBrowserFromCachingBookmarklets; var scriptsToLoad = defaultScripts.slice(0); // The bookmarklet should load scripts using the same protocol as the page. Otherwise browsers may give error messages about "mixed content", and refuse to load the script. var url = link.href.replace(/^http[s]*:/, "//"); scriptsToLoad.push(url); var neverCacheStr = ( neverCache ? "+'?dummy='+new Date().getTime()" : "" ); /* var toRun = "(function(){\n"; for (var i=0;i<scriptsToLoad.length;i++) { var script = scriptsToLoad[i]; toRun += " var newScript = document.createElement('script');\n"; toRun += " newScript.src = '" + script + "'" + neverCacheStr + ";\n"; toRun += " document.body.appendChild(newScript);\n"; } toRun += "})();"; */ var toRun = "(function(){\n"; // Chrome has no .toSource() or uneval(), so we use JSON. :f toRun += "var scriptsToLoad="+JSON.stringify(scriptsToLoad)+";\n"; toRun += "function loadNext() {\n"; toRun += " if (scriptsToLoad.length == 0) { return; }\n"; toRun += " var next = scriptsToLoad.shift();\n"; toRun += " var newScript = document.createElement('script');\n"; toRun += " newScript.src = next"+neverCacheStr+";\n"; toRun += " newScript.onload = loadNext;\n"; toRun += " newScript.onerror = function(e){ console.error('Problem loading script: '+next,e); };\n"; toRun += " document.body.appendChild(newScript);\n"; toRun += "}\n"; toRun += "loadNext();\n"; toRun += "})(); (void 0);"; var name = getNameFromFilename(link.href); /* if (neverCache) { name = name + " (NoCache)"; } if (includeGMCompat) { name = name + " (FBAPI)"; } */ var newLink = document.createElement("A"); newLink.href = "javascript:" + toRun; newLink.textContent = name; newLink.title = newLink.href; var newContainer = document.createElement("div"); // newContainer.style.whiteSpace = 'nowrap'; newContainer.appendChild(document.createTextNode("(Live Bookmarklet: ")); newContainer.appendChild(newLink); var extraString = ( neverCache || includeGMCompat ? neverCache && includeGMCompat ? " (no-caching, with GM fallbacks)" : neverCache ? " (no-caching)" : " (with GM fallbacks)" : "" ); // DISABLED HERE: extraString = ""; if (extraString) { // newContainer.appendChild(document.createTextNode(extraString)); // var extraText = document.createElement("span"); // extraText.style.fontSize = '80%'; var extraText = document.createElement("small"); extraText.textContent = extraString; newContainer.appendChild(extraText); } newContainer.appendChild(document.createTextNode(")")); newContainer.style.paddingLeft = '8px'; // link.parentNode.insertBefore(newContainer,link.nextSibling); return newContainer; } function reportMessage(msg) { console.log(msg); } function reportWarning(msg) { console.warn(msg); } function reportError(msg) { console.error(msg); } function doesItCompile(code) { try { var f = new Function(code); } catch (e) { return false; } return true; } /* For more bugs try putting (function(){ ... })(); wrapper around WikiIndent! */ function fixComments(line) { //// Clear // comment line line = line.replace(/^[ \t]*\/\/.*/g,''); //// Wrap // comment in /*...*/ (Dangerous! if comment contains a */ !) // line = line.replace(/^([ \t]*)\/\/(.*)/g,'$1/*$2*/'); //// Clear trailing comment (after a few chars we deem sensible) //// This still doesn't handle some valid cases. var trailingComment = /([;{}()\[\],\. \t])\s*\/\/.*/g; //// What might break: An odd number of "s or 's, any /*s or */s var worrying = /(["']|\/\*|\*\/)/; // Here is a breaking example: // var resNodes = document.evaluate("//div[@id='res']//li", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); // var hasTrailingComment = line.match(trailingComment); var hasTrailingComment = trailingComment.exec(line); if (hasTrailingComment) { /* if (line.match(worrying)) { reportWarning("Warning: trailingComment matches: "+hasTrailingComment); } */ var compiledBefore = doesItCompile(line); var newLine = line.replace(trailingComment,'$1'); var compilesAfter = doesItCompile(newLine); if (compiledBefore && !compilesAfter) { reportWarning("Aborted // stripping on: "+line); } else { // Accept changes line = newLine; } } return line; } function cleanupSource(source) { // console.log("old length: "+source.length); var lines = source.split('\n'); // console.log("lines: "+lines.length); for (var i=0;i<lines.length;i++) { lines[i] = fixComments(lines[i]); } // source = lines.join('\n'); source = lines.join(' '); // console.log("new length: "+source.length); //// For Bookmarklet Builder's reformatter: source = source.replace("(function","( function",'g'); return source; } // We could cache urls, at least during this one page visit // Specifically the ones we request repeatedly for statis bmls (fbgmapi). var sourcesLoaded = {}; // My first "promise": function getSourceFor(url) { return { then: function(handleResponse){ // Prevent caching of this resource. I found this was needed one time on Chrome! // It probably doesn't need to be linked to preventCachingOfInstallScripts or preventBrowserFromCachingBookmarklets. url += "?dummy="+Math.random(); console.log("Loading "+url); getURLThen(url, handlerFn, onErrorFn); function handlerFn(res) { var source = res.responseText; handleResponse(source); } function onErrorFn(res) { reportError("Failed to load "+url+": HTTP "+res.status); } // CONSIDER: We should return the next promise? } }; } /* To avoid a multitude of premature network requests, the bookmarklet is not actually "compiled" until mouseover. */ function buildStaticBookmarklet(link) { var newLink = document.createElement("a"); newLink.textContent = getNameFromFilename(link.href); var newContainer = document.createElement("div"); // newContainer.style.whiteSpace = 'nowrap'; // Experimental: newContainer.appendChild(document.createTextNode("(Static Bookmarklet: ")); newContainer.appendChild(newLink); newContainer.appendChild(document.createTextNode(")")); newContainer.style.paddingLeft = '8px'; newLink.style.textDecoration = 'underline'; // newLink.style.color = '#000080'; newLink.style.color = '#333366'; // link.parentNode.insertBefore(newContainer,link.nextSibling); // The href may change before we fire (e.g. if dummy is appended) so we make a copy. var href = link.href; // setTimeout(function(){ // ,2000 * staticsRequested); newLink.onmouseover = function(){ getStaticBookmarkletFromUserscript(href,whenGot); newLink.style.color = '#ff7700'; newLink.onmouseover = null; // once }; function whenGot(staticSrc, report) { var extraText = ""; // Does it parse? try { var testFn = new Function(staticSrc); newLink.style.color = ''; // Success! Color like a normal link } catch (e) { var msg = "PARSE FAILED"; // Firefox has this: if (e.lineNumber) { msg += " on line "+e.lineNumber; } msg += ":"; console.log("["+href+"] "+msg,e); newLink.title = msg+" "+e; newLink.style.color = 'red'; // color as error window.lastParseError = e; // return; } newLink.href = "javascript:" + staticSrc; if (addDateToStaticBookmarklets) { var d = new Date(); //var dateStr = d.getFullYear()+"."+(d.getMonth()+1)+"."+d.getDate(); var dateStr = d.toISOString().substring(0,10); newLink.textContent = newLink.textContent + " ("+dateStr+")"; } if (report.needsGMFallbacks) { extraText += " uses GM"; } else { extraText += " GM free"; } if (extraText) { newLink.parentNode.insertBefore( document.createTextNode(extraText), newLink.nextSibling); } // Just in case the browser is dumb, force it to re-analyse the link. newLink.parentNode.insertBefore(newLink, newLink.nextSibling); } return newContainer; } function getStaticBookmarkletFromUserscript(href, callbackGotStatic) { getSourceFor(href).then(function (userscriptSource) { var hasGrantNone = userscriptSource.match('\\n//\\s*@grant\\s+none\\s*\\n'); var needsGMFallbacks = ! hasGrantNone; var scriptsToLoad = needsGMFallbacks ? defaultScripts.slice(0) : []; loadScripts(scriptsToLoad, allSourcesLoaded); function allSourcesLoaded(scriptSources) { scriptSources.push(userscriptSource); var toRun = ""; for (var i=0; i<scriptSources.length; i++) { if (!scriptSources[i]) { reportError("Expected contents of "+scriptsToLoad[i]+" but got: "+scriptSources[i]); } var cleaned = cleanupSource(scriptSources[i]); toRun += "(function(){\n"; toRun += cleaned; toRun += "})();\n"; } toRun += "(void 0);"; var report = { needsGMFallbacks: needsGMFallbacks, }; callbackGotStatic(toRun, report); } }); function loadScripts(scriptsToLoad, callbackScriptsLoaded) { if (scriptsToLoad.length === 0) { return callbackScriptsLoaded([]); } var scriptSources = []; var numLoaded = 0; for (var i=0; i<scriptsToLoad.length; i++) { loadSourceIntoArray(i); } function loadSourceIntoArray(i) { getSourceFor(scriptsToLoad[i]).then(function(source){ if (!source) { reportError("Failed to acquire source for: "+href); } scriptSources[i] = source; numLoaded++; if (numLoaded == scriptsToLoad.length) { callbackScriptsLoaded(scriptSources); } }); } } } // In this case, link points to a folder containing a userscript. // We guess the userscript's name from the folder's name. function addQuickInstall(link) { if (link.parentNode.tagName == 'TD') { link.parentNode.style.width = '60%'; } var br2 = document.createElement("br"); link.parentNode.insertBefore(br2,link.nextSibling); var br = document.createElement("br"); link.parentNode.insertBefore(br,link.nextSibling.nextSibling); var name = link.href.match(/([^\/]*)\/$/)[1]; var newLink = document.createElement("A"); newLink.href = link.href + name+".user.js"; newLink.textContent = "Install Userscript"; // name+".user.js"; var newContainer = document.createElement("span"); newContainer.appendChild(document.createTextNode(" [")); newContainer.appendChild(newLink); newContainer.appendChild(document.createTextNode("]")); newContainer.style.paddingLeft = '8px'; link.parentNode.insertBefore(newContainer,br); link.style.color = 'black'; link.style.fontWeight = 'bold'; newContainer.appendChild(buildLiveBookmarklet(newLink)); newContainer.appendChild(buildStaticBookmarklet(newLink)); newContainer.appendChild(buildLiveUserscript(newLink)); popupSourceOnHover(newLink); // Do this after the other two builders have used the .href if (preventCachingOfInstallScripts) { newLink.href = newLink.href + '?dummy='+new Date().getTime(); } } function getURLThen(url,handlerFn,onErrorFn) { var req = new XMLHttpRequest(); req.open("get", url, true); req.onreadystatechange = function (aEvt) { if (req.readyState == 4) { if(req.status == 200) { // Got it handlerFn(req); } else { var msg = ("XHR failed with status "+req.status+"\n"); window.status = msg; onErrorFn(req); console.warn(msg); } } }; req.send(null); } var frame = null; function loadSourceViewer(url, newLink, evt) { // window.lastEVT = evt; if (frame && frame.parentNode) { frame.parentNode.removeChild(frame); } frame = document.createElement("div"); function reportToFrame(msg) { frame.appendChild( document.createTextNode(msg) ); } // This doesn't work. Loading it directly into the iframe triggers Greasemonkey to install it! //frame.src = url; // What we need to do is get the script with an XHR, then place it into the div. // This seems to fire immediately in Firefox! var cleanup = function(evt) { frame.parentNode.removeChild(frame); document.body.removeEventListener("click",cleanup,false); // frame.removeEventListener("mouseout",cleanup,false); }; document.body.addEventListener("click",cleanup,false); getURLThen(url, function(res){ // We were using pre instead of div to get monospace like <tt> or <code> // However since we are commonly reading the description, sans seems better. var displayDiv = document.createElement("div"); displayDiv.style.fontSize = '0.8em'; displayDiv.style.whiteSpace = "pre-wrap"; var displayCode = document.createElement("pre"); displayCode.textContent = res.responseText; displayCode.style.maxHeight = "100%"; displayCode.style.overflow = "auto"; displayDiv.appendChild(displayCode); while (frame.firstChild) { frame.removeChild(frame.firstChild); } frame.appendChild(displayDiv); if (typeof Rainbow != null) { /* // Works fine in Chrome, but in Firefox it causes Rainbow to fail with "too much recursion". Rainbow.extend('javascript', [ { 'name': 'importantcomment', 'pattern': /(\/\/|\#) @(name|description|include) [\s\S]*?$/gm }, ], false); */ setTimeout(function(){ displayCode.setAttribute('data-language', "javascript"); displayCode.style.fontSize = '100%'; Rainbow.color(displayCode.parentNode, function(){ console.log("Rainbow finished."); }); },50); } // frame.addEventListener("mouseout",cleanup,false); // newLink.title = res.responseText; var lines = res.responseText.split("\n"); for (var i=0;i<lines.length;i++) { var line = lines[i]; if (line.match(/@description\s/)) { var descr = line.replace(/.*@description\s*/,''); newLink.title = descr; break; } } }, function(res){ reportToFrame("Failed to load "+url+": HTTP "+res.status); }); /* frame.style.position = 'fixed'; frame.style.top = evt.clientY+4+'px'; frame.style.left = evt.clientX+4+'px'; */ // frame.style.position = 'absolute'; // frame.style.top = evt.layerY+12+'px'; // frame.style.left = evt.layerX+12+'px'; // frame.style.top = evt.layerY - window.innerHeight*35/100 + 'px'; // frame.style.left = evt.layerX + 64 + 'px'; // frame.style.width = "70%"; // frame.style.height = "70%"; frame.style.position = 'fixed'; frame.style.right = '2%'; frame.style.width = '50%'; frame.style.top = '10%'; frame.style.height = '80%'; frame.style.backgroundColor = 'white'; frame.style.color = 'black'; frame.style.padding = '8px'; frame.style.border = '2px solid #555555'; document.body.appendChild(frame); reportToFrame("Loading..."); } function buildSourceViewer(link) { var newLink = document.createElement("A"); // newLink.href = '#'; newLink.textContent = "Source"; newLink.addEventListener('click',function(e) { loadSourceViewer(link.href,newLink,e); },false); // TODO: Problem with .user.js files and Chrome: // In Chrome, opens an empty iframe then the statusbar says it wants to install an extension. // For Chrome we could try: frame.src = "view-source:"+...; var extra = document.createElement("span"); extra.appendChild(document.createTextNode("[")); extra.appendChild(newLink); extra.appendChild(document.createTextNode("]")); extra.style.paddingLeft = '8px'; // link.parentNode.insertBefore(extra,link.nextSibling); return extra; } function popupSourceOnHover(link) { var hoverTimer = null; function startHover(evt) { stopHover(evt); hoverTimer = setTimeout(function(){ loadSourceViewer(link.href, link, evt); stopHover(evt); // link.removeEventListener("mouseover",startHover,false); // link.removeEventListener("mouseout",stopHover,false); },1500); } function stopHover(evt) { clearTimeout(hoverTimer); hoverTimer = null; } link.addEventListener("mouseover",startHover,false); link.addEventListener("mouseout",stopHover,false); // If they click on it before waiting to hover, they probably don't want the popup: link.addEventListener("click",stopHover,false); } function buildLiveUserscript(link) { //// This isn't working any more. data:// lost its power circa 2006 due to abuse. //// Create a clickable link that returns a sort-of file to the browser using the "data:" protocol. //// That file would be a new userscript for installation. //// We can generate the contents of this new userscript at run-time. //// The current one we generate runs (no @includes), and loads the latest userscript from its website via script injection. /* DISABLED // BUG: data:{...}.user.js does not interest my Chromium var name = getNameFromFilename(link.href)+" Live!"; var name = "Install LiveLoader"; var whatToRun = '(function(){\n' + ' var ns = document.createElement("script");\n' + ' ns.src = "' + encodeURI(getCanonicalUrl(link.href)) + '";\n' + ' document.getElementsByTagName("head")[0].appendChild(ns);\n' + '})();\n'; var newLink = document.createElement("a"); newLink.textContent = name; newLink.href = "data:text/javascript;charset=utf-8," + "// ==UserScript==%0A" + "// @namespace LiveLoader%0A" + "// @name " + name + " LIVE%0A" + "// @description Loads " +name+ " userscript live from " + link.href + "%0A" + "// ==/UserScript==%0A" + "%0A" + encodeURIComponent(whatToRun) + "%0A" + "//.user.js"; var extra = document.createElement("span"); extra.appendChild(document.createTextNode("[")); extra.appendChild(newLink); extra.appendChild(document.createTextNode("]")); extra.style.paddingLeft = '8px'; link.parentNode.insertBefore(extra,link.nextSibling); */ return document.createTextNode(""); } function getCanonicalUrl(url) { if (url.substring(0,1)=="/") { url = document.location.protocol + "://" + document.location.domain + "/" + url; } if (!url.match("://")) { url = document.location.href.match("^[^?]*/") + url; } return url; } function getNameFromFilename(href) { var isUserscript = href.match(/\.user\.js$/); // Remove any leading folders and trailing ".user.js" var name = href.match(/[^\/]*$/)[0].replace(/\.user\.js$/,''); name = decodeURIComponent(name); // The scripts on userscripts.org do not have their name in the filename, // but we can get the name from the page title! if (document.location.host=="userscripts.org" && document.location.pathname=="/scripts/show/"+name) { var scriptID = name; name = document.title.replace(/ for Greasemonkey/,''); // Optionally, include id in name: name += " ("+scriptID+")"; } if (isUserscript) { var words = name.split("_"); for (var i=0;i<words.length;i++) { if (words[i].length) { var c = words[i].charCodeAt(0); if (c>=97 && c<=122) { c = 65 + (c - 97); words[i] = String.fromCharCode(c) + words[i].substring(1); } } } name = words.join(" "); } else { // It's just a Javascript file name = "Load "+name; } return name; } var links = document.getElementsByTagName("A"); //// We used to process backwards (for less height recalculation). //// But this was messing up maxStaticsToRequest. //// But now backwards processing is restored, to avoid producing multiple bookmarks! for (var i=links.length;i--;) { //for (var i=0;i<links.length;i++) { var link = links[i]; if (link.getAttribute('data-make-bookmarklet') === 'false') { continue; } // If we see a direct link to a user script, create buttons for it. if (link.href.match(/\.js$/)) { // \.user\.js var where = link; function insert(newElem) { where.parentNode.insertBefore(newElem,where.nextSibling); where = newElem; } insert(buildLiveBookmarklet(link)); insert(buildStaticBookmarklet(link)); insert(buildLiveUserscript(link)); insert(buildSourceViewer(link)); } // If the current page looks like a Greasemonkey Userscript Folder, then // create an installer for every subfolder (assuming a script is inside it). if (document.location.pathname.match(/\/(gm_scripts|userscripts)\//)) { if (link.href.match(/\/$/) && link.textContent!=="Parent Directory") { addQuickInstall(link); } } } /* var promise(getURLThen,url) { var handler; getURLThen(url,handlerFn,handlerFn); function handlerFn(res) { var source = res.responseText; if (handler) { handler(source); } else { reportError("No handler set for: "+ } } return { then: function(handleResponse){ handler = handleResponse; } }; } */