Greasy Fork is available in English.

Make Bookmarklets from Javascript URLs

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).

  1. // ==UserScript==
  2. // @name Make Bookmarklets from Javascript URLs
  3. // @namespace MBFU
  4. // @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).
  5. // @include http://hwi.ath.cx/*/gm_scripts/*
  6. // @include http://hwi.ath.cx/*/userscripts/*
  7. // @include http://*userscripts.org/*
  8. // @include https://*userscripts.org/*
  9. // @include http://openuserjs.org/*
  10. // @include https://openuserjs.org/*
  11. // @include http://greatest.deepsurf.us/*
  12. // @include https://greatest.deepsurf.us/*
  13. // @include http://*/*
  14. // @include https://*/*
  15. // @exclude http://hwi.ath.cx/code/other/gm_scripts/joeys_userscripts_and_bookmarklets_overview.html
  16. // @exclude http://neuralyte.org/~joey/gm_scripts/joeys_userscripts_and_bookmarklets_overview.html
  17. // @exclude http://joeytwiddle.github.io/code/other/gm_scripts/joeys_userscripts_and_bookmarklets_overview.html
  18. // @grant none
  19. // @version 1.2.8
  20. // ==/UserScript==
  21.  
  22. // 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.
  23.  
  24. // 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.
  25. // Static bookmarklets could timeout and re-build after ... 10 seconds? ^^
  26.  
  27. // Most people will NOT want the NoCache version. It's only useful for script developers.
  28.  
  29. // Firefox/Greasemonkey does not like to install scripts ending ".user.js?dummy=123"
  30. // But in Chrome it is a useful way to prevent the script from being cached when you are developing it.
  31. var inGoogleChrome = window && window.navigator && window.navigator.vendor.match(/Google/);
  32.  
  33. // It doesn't make much sense for me to make it browser-dependent, because I share my bookmarks between Chrome and Firefox (using XMarks).
  34. var preventBrowserFromCachingBookmarklets = inGoogleChrome;
  35. //var preventBrowserFromCachingBookmarklets = true;
  36.  
  37. // Sometimes Chrome refuses to acknowledge that a script has been updated, and repeatedly installs an old version from its cache!
  38. var preventCachingOfInstallScripts = inGoogleChrome;
  39. // 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+"?
  40.  
  41. var addGreasemonkeyLibToBookmarklets = true;
  42.  
  43. // If enabled, as well as *.user.js files, it will also offer bookmarklets for *.js files
  44. var activateOnAllJavascriptFiles = false;
  45.  
  46. // DONE: All bookmarklets optionally preload the Fallback GMAPI.
  47. // DONE: All bookmarklets optionally load in non-caching fashion (for changing scripts).
  48.  
  49. // DONE: Use onload event to ensure prerequisite scripts are loaded before dependent scripts.
  50.  
  51. // DONE via FBGMAPI: We could provide neat catches for GM_ API commands so they won't fail entirely.
  52.  
  53. // TODO: Provide extra feature, which allows the Bookmarks to actually trigger
  54. // the userscript properly-running inside Greasemonkey, if this userscript is
  55. // present to handle the request, otherwise (with warning) load outside GM.
  56.  
  57. // TODO: Support @include/@excude and @require meta rules?
  58. // This requires parsing the script's header using XHR before creating each bookmarklet.
  59.  
  60. // DONE: Optionally create static bookmarklet, with all code inline, rather than loaded from a URL.
  61. // // comments will need to be removed or converted to /*...*/ comments
  62.  
  63. var addDateToStaticBookmarklets = true;
  64.  
  65. var defaultScripts = [];
  66. var includeGMCompat = addGreasemonkeyLibToBookmarklets;
  67. if (includeGMCompat) {
  68. // defaultScripts.push("http://hwi.ath.cx/code/other/gm_scripts/fallbackgmapi/fallbackgmapi.user.js");
  69. // defaultScripts.push("//neuralyte.org/~joey/gm_scripts/fallbackgmapi/fallbackgmapi.user.js");
  70. defaultScripts.push("//joeytwiddle.github.io/code/other/gm_scripts/fallbackgmapi/fallbackgmapi.user.js");
  71. }
  72.  
  73. function buildLiveBookmarklet(url) {
  74. var neverCache = preventBrowserFromCachingBookmarklets;
  75.  
  76. var scriptsToLoad = defaultScripts.slice(0);
  77.  
  78. // 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.
  79. url = url.replace(/^http[s]*:[/][/]/, "//");
  80. scriptsToLoad.push(url);
  81.  
  82. var neverCacheStr = ( neverCache ? "+'?dummy='+new Date().getTime()" : "" );
  83.  
  84. /*
  85. var toRun = "(function(){\n";
  86. for (var i=0;i<scriptsToLoad.length;i++) {
  87. var script = scriptsToLoad[i];
  88. toRun += " var newScript = document.createElement('script');\n";
  89. toRun += " newScript.src = '" + script + "'" + neverCacheStr + ";\n";
  90. toRun += " document.body.appendChild(newScript);\n";
  91. }
  92. toRun += "})();";
  93. */
  94.  
  95. var toRun = "(function(){\n";
  96. // Chrome has no .toSource() or uneval(), so we use JSON. :f
  97. toRun += "var scriptsToLoad="+JSON.stringify(scriptsToLoad)+";\n";
  98. toRun += "function loadNext() {\n";
  99. toRun += " if (scriptsToLoad.length == 0) { return; }\n";
  100. toRun += " var next = scriptsToLoad.shift();\n";
  101. toRun += " var newScript = document.createElement('script');\n";
  102. toRun += " newScript.src = next"+neverCacheStr+";\n";
  103. toRun += " newScript.onload = loadNext;\n";
  104. toRun += " newScript.onerror = function(e){ console.error('Problem loading script: '+next,e); };\n";
  105. toRun += " document.body.appendChild(newScript);\n";
  106. toRun += "}\n";
  107. toRun += "loadNext();\n";
  108. toRun += "})(); (void 0);";
  109.  
  110. var name = getNameFromFilename(url);
  111. /*
  112. if (neverCache) {
  113. name = name + " (NoCache)";
  114. }
  115. if (includeGMCompat) {
  116. name = name + " (FBAPI)";
  117. }
  118. */
  119.  
  120. var newLink = document.createElement("A");
  121. newLink.href = "javascript:" + toRun;
  122. newLink.textContent = name;
  123. newLink.title = newLink.href;
  124.  
  125. var newContainer = document.createElement("div");
  126. // Act like a span, do not wrap onto separate lines
  127. // Removed because it was problematic on some sites (which?)
  128. //newContainer.style.whiteSpace = 'nowrap';
  129. newContainer.appendChild(document.createTextNode("(Live Bookmarklet: "));
  130. newContainer.appendChild(newLink);
  131. var extraString = ( neverCache || includeGMCompat ? neverCache && includeGMCompat ? " (no-caching, with GM fallbacks)" : neverCache ? " (no-caching)" : " (with GM fallbacks)" : "" );
  132. // DISABLED HERE:
  133. extraString = "";
  134. if (extraString) {
  135. // newContainer.appendChild(document.createTextNode(extraString));
  136. // var extraText = document.createElement("span");
  137. // extraText.style.fontSize = '80%';
  138. var extraText = document.createElement("small");
  139. extraText.textContent = extraString;
  140. newContainer.appendChild(extraText);
  141. }
  142. newContainer.appendChild(document.createTextNode(")"));
  143. newContainer.style.paddingLeft = '8px';
  144. // link.parentNode.insertBefore(newContainer,link.nextSibling);
  145. return newContainer;
  146. }
  147.  
  148. function reportMessage(msg) {
  149. console.log(msg);
  150. }
  151.  
  152. function reportWarning(msg) {
  153. console.warn(msg);
  154. }
  155.  
  156. function reportError(msg) {
  157. console.error(msg);
  158. }
  159.  
  160. function doesItCompile(code) {
  161. try {
  162. var f = new Function(code);
  163. } catch (e) {
  164. return false;
  165. }
  166. return true;
  167. }
  168.  
  169. /* For more bugs try putting (function(){ ... })(); wrapper around WikiIndent! */
  170.  
  171. function fixComments(line) {
  172.  
  173. //// Clear // comment line
  174. line = line.replace(/^[ \t]*\/\/.*/g,'');
  175.  
  176. //// Wrap // comment in /*...*/ (Dangerous! if comment contains a */ !)
  177. // line = line.replace(/^([ \t]*)\/\/(.*)/g,'$1/*$2*/');
  178.  
  179. //// Clear trailing comment (after a few chars we deem sensible)
  180. //// This still doesn't handle some valid cases.
  181. var trailingComment = /([;{}()\[\],\. \t])\s*\/\/.*/g;
  182. //// What might break: An odd number of "s or 's, any /*s or */s
  183. var worrying = /(["']|\/\*|\*\/)/;
  184. // Here is a breaking example:
  185. // var resNodes = document.evaluate("//div[@id='res']//li", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  186. // var hasTrailingComment = line.match(trailingComment);
  187. var hasTrailingComment = trailingComment.exec(line);
  188. if (hasTrailingComment) {
  189. /*
  190. if (line.match(worrying)) {
  191. reportWarning("Warning: trailingComment matches: "+hasTrailingComment);
  192. }
  193. */
  194. var compiledBefore = doesItCompile(line);
  195. var newLine = line.replace(trailingComment,'$1');
  196. var compilesAfter = doesItCompile(newLine);
  197. if (compiledBefore && !compilesAfter) {
  198. reportWarning("Aborted // stripping on: "+line);
  199. } else {
  200. // Accept changes
  201. line = newLine;
  202. }
  203. }
  204. return line;
  205.  
  206. }
  207.  
  208. function cleanupSource(source) {
  209. // console.log("old length: "+source.length);
  210. var lines = source.split('\n');
  211. // console.log("lines: "+lines.length);
  212. for (var i=0;i<lines.length;i++) {
  213. lines[i] = fixComments(lines[i]);
  214. }
  215. // source = lines.join('\n');
  216. source = lines.join(' ');
  217. // console.log("new length: "+source.length);
  218. //// For Bookmarklet Builder's reformatter:
  219. source = source.replace("(function","( function",'g');
  220. return source;
  221. }
  222.  
  223. // We could cache urls, at least during this one page visit
  224. // Specifically the ones we request repeatedly for statis bmls (fbgmapi).
  225. var sourcesLoaded = {};
  226.  
  227. // My first "promise":
  228. function getSourceFor(url) {
  229. return {
  230. then: function(handleResponse){
  231. // Prevent caching of this resource. I found this was needed one time on Chrome!
  232. // It probably doesn't need to be linked to preventCachingOfInstallScripts or preventBrowserFromCachingBookmarklets.
  233. url += "?dummy="+Math.random();
  234. console.log("Loading "+url);
  235. getURLThen(url, handlerFn, onErrorFn);
  236.  
  237. function handlerFn(res) {
  238. var source = res.responseText;
  239. handleResponse(source);
  240. }
  241.  
  242. function onErrorFn(res) {
  243. reportError("Failed to load "+url+": HTTP "+res.status);
  244. }
  245.  
  246. // CONSIDER: We should return the next promise?
  247. }
  248. };
  249. }
  250.  
  251. /* To avoid a multitude of premature network requests, the bookmarklet is not actually "compiled" until mouseover. */
  252. function buildStaticBookmarklet(url) {
  253.  
  254. var newLink = document.createElement("a");
  255. newLink.textContent = getNameFromFilename(url);
  256. newLink.style.cursor = 'pointer';
  257.  
  258. var newContainer = document.createElement("div");
  259. // newContainer.style.whiteSpace = 'nowrap';
  260. // Experimental:
  261. newContainer.appendChild(document.createTextNode("(Static Bookmarklet: "));
  262. newContainer.appendChild(newLink);
  263. newContainer.appendChild(document.createTextNode(")"));
  264. newContainer.style.paddingLeft = '8px';
  265.  
  266. newLink.style.textDecoration = 'underline';
  267. // newLink.style.color = '#000080';
  268. newLink.style.color = '#333366';
  269.  
  270. // link.parentNode.insertBefore(newContainer,link.nextSibling);
  271.  
  272. // The href may change before we fire (e.g. if dummy is appended) so we make a copy.
  273. var href = url;
  274.  
  275. // setTimeout(function(){
  276. // ,2000 * staticsRequested);
  277. newLink.onmouseover = function(){
  278. getStaticBookmarkletFromUserscript(href,whenGot);
  279. newLink.style.color = '#ff7700';
  280. newLink.onmouseover = null; // once
  281. };
  282.  
  283. function whenGot(staticSrc, report) {
  284.  
  285. var extraText = "";
  286.  
  287. // Does it parse?
  288. try {
  289. var testFn = new Function(staticSrc);
  290. newLink.style.color = ''; // Success! Color like a normal link
  291. } catch (e) {
  292. var msg = "PARSE FAILED";
  293. // Firefox has this:
  294. if (e.lineNumber) {
  295. msg += " on line "+e.lineNumber;
  296. }
  297. msg += ":";
  298. console.log("["+href+"] "+msg,e);
  299. newLink.title = msg+" "+e;
  300. newLink.style.color = 'red'; // color as error
  301. window.lastParseError = e;
  302. // return;
  303. }
  304.  
  305. newLink.href = "javascript:" + staticSrc;
  306.  
  307. if (addDateToStaticBookmarklets) {
  308. var d = new Date();
  309. //var dateStr = d.getFullYear()+"."+(d.getMonth()+1)+"."+d.getDate();
  310. var dateStr = d.toISOString().substring(0,10);
  311. newLink.textContent = newLink.textContent + " ("+dateStr+")";
  312. }
  313.  
  314. if (report.needsGMFallbacks) {
  315. extraText += " uses GM";
  316. } else {
  317. extraText += " GM free";
  318. }
  319.  
  320. if (extraText) {
  321. newLink.parentNode.insertBefore( document.createTextNode(extraText), newLink.nextSibling);
  322. }
  323.  
  324. // Just in case the browser is dumb, force it to re-analyse the link.
  325. newLink.parentNode.insertBefore(newLink, newLink.nextSibling);
  326. }
  327.  
  328. return newContainer;
  329.  
  330. }
  331.  
  332. function getStaticBookmarkletFromUserscript(href, callbackGotStatic) {
  333.  
  334. getSourceFor(href).then(function (userscriptSource) {
  335. var hasGrantNone = userscriptSource.match('\\n//\\s*@grant\\s+none\\s*\\n');
  336. var needsGMFallbacks = ! hasGrantNone;
  337.  
  338. var scriptsToLoad = needsGMFallbacks ? defaultScripts.slice(0) : [];
  339.  
  340. loadScripts(scriptsToLoad, allSourcesLoaded);
  341.  
  342. function allSourcesLoaded(scriptSources) {
  343. scriptSources.push(userscriptSource);
  344.  
  345. var toRun = "";
  346. for (var i=0; i<scriptSources.length; i++) {
  347. if (!scriptSources[i]) {
  348. reportError("Expected contents of "+scriptsToLoad[i]+" but got: "+scriptSources[i]);
  349. }
  350. var cleaned = cleanupSource(scriptSources[i]);
  351. toRun += "(function(){\n";
  352. toRun += cleaned;
  353. toRun += "})();\n";
  354. }
  355. toRun += "(void 0);";
  356.  
  357. var report = {
  358. needsGMFallbacks: needsGMFallbacks,
  359. };
  360.  
  361. callbackGotStatic(toRun, report);
  362. }
  363. });
  364.  
  365. function loadScripts(scriptsToLoad, callbackScriptsLoaded) {
  366. if (scriptsToLoad.length === 0) {
  367. return callbackScriptsLoaded([]);
  368. }
  369.  
  370. var scriptSources = [];
  371. var numLoaded = 0;
  372.  
  373. for (var i=0; i<scriptsToLoad.length; i++) {
  374. loadSourceIntoArray(i);
  375. }
  376.  
  377. function loadSourceIntoArray(i) {
  378. getSourceFor(scriptsToLoad[i]).then(function(source){
  379. if (!source) {
  380. reportError("Failed to acquire source for: "+href);
  381. }
  382. scriptSources[i] = source;
  383. numLoaded++;
  384. if (numLoaded == scriptsToLoad.length) {
  385. callbackScriptsLoaded(scriptSources);
  386. }
  387. });
  388. }
  389. }
  390.  
  391. }
  392.  
  393. // In this case, link points to a folder containing a userscript.
  394. // We guess the userscript's name from the folder's name.
  395. function addQuickInstall(link) {
  396. if (link.parentNode.tagName == 'TD') {
  397. link.parentNode.style.width = '60%';
  398. }
  399. var br2 = document.createElement("br");
  400. link.parentNode.insertBefore(br2,link.nextSibling);
  401. var br = document.createElement("br");
  402. link.parentNode.insertBefore(br,link.nextSibling.nextSibling);
  403. var name = link.href.match(/([^\/]*)\/$/)[1];
  404. var newLink = document.createElement("A");
  405. newLink.href = link.href + name+".user.js";
  406. newLink.textContent = "Install Userscript"; // name+".user.js";
  407. var newContainer = document.createElement("span");
  408. newContainer.appendChild(document.createTextNode(" ["));
  409. newContainer.appendChild(newLink);
  410. newContainer.appendChild(document.createTextNode("]"));
  411. newContainer.style.paddingLeft = '8px';
  412. link.parentNode.insertBefore(newContainer,br);
  413. link.style.color = 'black';
  414. link.style.fontWeight = 'bold';
  415. newContainer.appendChild(buildLiveBookmarklet(newLink.href));
  416. newContainer.appendChild(buildStaticBookmarklet(newLink.href));
  417. newContainer.appendChild(buildLiveUserscript(newLink));
  418. popupSourceOnHover(newLink);
  419. // Do this after the other two builders have used the .href
  420. if (preventCachingOfInstallScripts) {
  421. newLink.href = newLink.href + '?dummy='+new Date().getTime();
  422. }
  423. }
  424.  
  425. function getURLThen(url,handlerFn,onErrorFn) {
  426. var req = new XMLHttpRequest();
  427. req.open("get", url, true);
  428. req.onreadystatechange = function (aEvt) {
  429. if (req.readyState == 4) {
  430. if(req.status == 200) {
  431. // Got it
  432. handlerFn(req);
  433. } else {
  434. var msg = ("XHR failed with status "+req.status+"\n");
  435. window.status = msg;
  436. onErrorFn(req);
  437. console.warn(msg);
  438. }
  439. }
  440. };
  441. req.send(null);
  442. }
  443.  
  444. var frame = null;
  445.  
  446. function loadSourceViewer(url, newLink, evt) {
  447.  
  448. // window.lastEVT = evt;
  449.  
  450. if (frame && frame.parentNode) {
  451. frame.parentNode.removeChild(frame);
  452. }
  453. frame = document.createElement("div");
  454.  
  455. function reportToFrame(msg) {
  456. frame.appendChild( document.createTextNode(msg) );
  457. }
  458.  
  459. // This doesn't work. Loading it directly into the iframe triggers Greasemonkey to install it!
  460. //frame.src = url;
  461. // What we need to do is get the script with an XHR, then place it into the div.
  462.  
  463. // This seems to fire immediately in Firefox!
  464. var cleanup = function(evt) {
  465. frame.parentNode.removeChild(frame);
  466. document.body.removeEventListener("click",cleanup,false);
  467. // frame.removeEventListener("mouseout",cleanup,false);
  468. };
  469. document.body.addEventListener("click",cleanup,false);
  470.  
  471. getURLThen(url, function(res){
  472. // We were using pre instead of div to get monospace like <tt> or <code>
  473. // However since we are commonly reading the description, sans seems better.
  474. var displayDiv = document.createElement("div");
  475. displayDiv.style.fontSize = '0.8em';
  476. displayDiv.style.whiteSpace = "pre-wrap";
  477. displayDiv.style.height = "100%";
  478. displayDiv.style.overflow = "auto";
  479. var displayCode = document.createElement("pre");
  480. displayCode.textContent = res.responseText;
  481. //displayCode.style.maxHeight = "100%";
  482. //displayCode.style.overflow = "auto";
  483. displayDiv.appendChild(displayCode);
  484. while (frame.firstChild) {
  485. frame.removeChild(frame.firstChild);
  486. }
  487. frame.appendChild(displayDiv);
  488. if (typeof Rainbow != null) {
  489. /*
  490. // Works fine in Chrome, but in Firefox it causes Rainbow to fail with "too much recursion".
  491. Rainbow.extend('javascript', [
  492. {
  493. 'name': 'importantcomment',
  494. 'pattern': /(\/\/|\#) @(name|description|include) [\s\S]*?$/gm
  495. },
  496. ], false);
  497. */
  498. setTimeout(function(){
  499. displayCode.setAttribute('data-language', "javascript");
  500. displayCode.style.fontSize = '100%';
  501. Rainbow.color(displayCode.parentNode, function(){
  502. console.log("Rainbow finished.");
  503. });
  504. },50);
  505. }
  506. // frame.addEventListener("mouseout",cleanup,false);
  507. // newLink.title = res.responseText;
  508. var lines = res.responseText.split("\n");
  509. for (var i=0;i<lines.length;i++) {
  510. var line = lines[i];
  511. if (line.match(/@description\s/)) {
  512. var descr = line.replace(/.*@description\s*/,'');
  513. newLink.title = descr;
  514. break;
  515. }
  516. }
  517. }, function(res){
  518. reportToFrame("Failed to load "+url+": HTTP "+res.status);
  519. });
  520.  
  521. /*
  522. frame.style.position = 'fixed';
  523. frame.style.top = evt.clientY+4+'px';
  524. frame.style.left = evt.clientX+4+'px';
  525. */
  526. // frame.style.position = 'absolute';
  527. // frame.style.top = evt.layerY+12+'px';
  528. // frame.style.left = evt.layerX+12+'px';
  529. // frame.style.top = evt.layerY - window.innerHeight*35/100 + 'px';
  530. // frame.style.left = evt.layerX + 64 + 'px';
  531. // frame.style.width = "70%";
  532. // frame.style.height = "70%";
  533. frame.style.position = 'fixed';
  534. frame.style.right = '2%';
  535. frame.style.width = '50%';
  536. frame.style.top = '10%';
  537. frame.style.height = '80%';
  538. frame.style.backgroundColor = 'white';
  539. frame.style.color = 'black';
  540. frame.style.padding = '8px';
  541. frame.style.border = '2px solid #555555';
  542. document.body.appendChild(frame);
  543.  
  544. reportToFrame("Loading...");
  545.  
  546. }
  547.  
  548. function buildSourceViewer(url) {
  549. var newLink = document.createElement("A");
  550. // newLink.href = '#';
  551. newLink.textContent = "Source";
  552.  
  553. newLink.addEventListener('click',function(e) {
  554. loadSourceViewer(url, newLink, e);
  555. },false);
  556.  
  557. // TODO: Problem with .user.js files and Chrome:
  558. // In Chrome, opens an empty iframe then the statusbar says it wants to install an extension.
  559. // For Chrome we could try: frame.src = "view-source:"+...;
  560. var extra = document.createElement("span");
  561. extra.appendChild(document.createTextNode("["));
  562. extra.appendChild(newLink);
  563. extra.appendChild(document.createTextNode("]"));
  564. extra.style.paddingLeft = '8px';
  565.  
  566. // link.parentNode.insertBefore(extra,link.nextSibling);
  567. return extra;
  568. }
  569.  
  570. function popupSourceOnHover(link) {
  571. var hoverTimer = null;
  572. function startHover(evt) {
  573. stopHover(evt);
  574. hoverTimer = setTimeout(function(){
  575. loadSourceViewer(link.href, link, evt);
  576. stopHover(evt);
  577. // link.removeEventListener("mouseover",startHover,false);
  578. // link.removeEventListener("mouseout",stopHover,false);
  579. },1500);
  580. }
  581. function stopHover(evt) {
  582. clearTimeout(hoverTimer);
  583. hoverTimer = null;
  584. }
  585. link.addEventListener("mouseover",startHover,false);
  586. link.addEventListener("mouseout",stopHover,false);
  587. // If they click on it before waiting to hover, they probably don't want the popup:
  588. link.addEventListener("click",stopHover,false);
  589. }
  590.  
  591. function buildLiveUserscript(link) {
  592. //// This isn't working any more. data:// lost its power circa 2006 due to abuse.
  593. //// Create a clickable link that returns a sort-of file to the browser using the "data:" protocol.
  594. //// That file would be a new userscript for installation.
  595. //// We can generate the contents of this new userscript at run-time.
  596. //// The current one we generate runs (no @includes), and loads the latest userscript from its website via script injection.
  597. /* DISABLED
  598. // BUG: data:{...}.user.js does not interest my Chromium
  599. var name = getNameFromFilename(link.href)+" Live!";
  600. var name = "Install LiveLoader";
  601. var whatToRun = '(function(){\n'
  602. + ' var ns = document.createElement("script");\n'
  603. + ' ns.src = "' + encodeURI(getCanonicalUrl(link.href)) + '";\n'
  604. + ' document.getElementsByTagName("head")[0].appendChild(ns);\n'
  605. + '})();\n';
  606. var newLink = document.createElement("a");
  607. newLink.textContent = name;
  608. newLink.href = "data:text/javascript;charset=utf-8,"
  609. + "// ==UserScript==%0A"
  610. + "// @namespace LiveLoader%0A"
  611. + "// @name " + name + " LIVE%0A"
  612. + "// @description Loads " +name+ " userscript live from " + link.href + "%0A"
  613. + "// ==/UserScript==%0A"
  614. + "%0A"
  615. + encodeURIComponent(whatToRun) + "%0A"
  616. + "//.user.js";
  617. var extra = document.createElement("span");
  618. extra.appendChild(document.createTextNode("["));
  619. extra.appendChild(newLink);
  620. extra.appendChild(document.createTextNode("]"));
  621. extra.style.paddingLeft = '8px';
  622. link.parentNode.insertBefore(extra,link.nextSibling);
  623. */
  624. return document.createTextNode("");
  625. }
  626.  
  627. function getCanonicalUrl(url) {
  628. if (url.substring(0,1)=="/") {
  629. url = document.location.protocol + "://" + document.location.domain + "/" + url;
  630. }
  631. if (!url.match("://")) {
  632. url = document.location.href.match("^[^?]*/") + url;
  633. }
  634. return url;
  635. }
  636.  
  637. function getNameFromFilename(href) {
  638. var isUserscript = href.match(/\.user\.js$/);
  639. // Remove any leading folders and trailing ".user.js"
  640. var name = href.match(/[^\/]*$/)[0].replace(/\.user\.js$/,'');
  641.  
  642. name = decodeURIComponent(name);
  643.  
  644. // The scripts on userscripts.org do not have their name in the filename,
  645. // but we can get the name from the page title!
  646. if (document.location.host=="userscripts.org" && document.location.pathname=="/scripts/show/"+name) {
  647. var scriptID = name;
  648. name = document.title.replace(/ for Greasemonkey/,'');
  649. // Optionally, include id in name:
  650. name += " ("+scriptID+")";
  651. }
  652.  
  653. if (isUserscript) {
  654. var words = name.split("_");
  655. for (var i=0;i<words.length;i++) {
  656. if (words[i].length) {
  657. var c = words[i].charCodeAt(0);
  658. if (c>=97 && c<=122) {
  659. c = 65 + (c - 97);
  660. words[i] = String.fromCharCode(c) + words[i].substring(1);
  661. }
  662. }
  663. }
  664. name = words.join(" ");
  665. } else {
  666. // It's just a Javascript file
  667. name = "Load "+name;
  668. }
  669. return name;
  670. }
  671.  
  672. var links = document.getElementsByTagName("A");
  673. //// We used to process backwards (for less height recalculation).
  674. //// But this was messing up maxStaticsToRequest.
  675. //// But now backwards processing is restored, to avoid producing multiple bookmarks!
  676. for (var i=links.length;i--;) {
  677. //for (var i=0;i<links.length;i++) {
  678. var link = links[i];
  679.  
  680. if (link.getAttribute('data-make-bookmarklet') === 'false') {
  681. continue;
  682. }
  683.  
  684. var regexp = activateOnAllJavascriptFiles ? /\.js$/ : /\.user\.js/;
  685.  
  686. // If we see a direct link to a user script, create buttons for it.
  687. if (link.href.match(regexp)) {
  688. var url = link.href;
  689.  
  690. /*
  691. if (link.hostname === 'github.com' && !link.href.match(/[/]raw[/]/)) {
  692. // Ignore links to github.com/foo/bar/blob/master/script.js
  693. // Accept links to github.com/foo/bar/raw/master/script.js
  694. continue;
  695. }
  696. */
  697.  
  698. // Turn github /blob/ links into /raw/ links
  699. if (link.hostname === 'github.com' && !link.href.match(/[/]raw[/]/)) {
  700. url = url.replace(/[/]blob[/]/, '/raw/');
  701. }
  702.  
  703. var where = link;
  704. function insert(newElem) {
  705. where.parentNode.insertBefore(newElem,where.nextSibling);
  706. where = newElem;
  707. }
  708. insert(buildLiveBookmarklet(url));
  709. insert(buildStaticBookmarklet(url));
  710. insert(buildLiveUserscript(link));
  711. insert(buildSourceViewer(url));
  712. }
  713.  
  714. // If the current page looks like a Greasemonkey Userscript Folder, then
  715. // create an installer for every subfolder (assuming a script is inside it).
  716. if (document.location.pathname.match(/\/(gm_scripts|userscripts)\//)) {
  717. if (link.href.match(/\/$/) && link.textContent!=="Parent Directory") {
  718. addQuickInstall(link);
  719. }
  720. }
  721.  
  722. }
  723.  
  724. /*
  725. var promise(getURLThen,url) {
  726. var handler;
  727.  
  728. getURLThen(url,handlerFn,handlerFn);
  729.  
  730. function handlerFn(res) {
  731. var source = res.responseText;
  732. if (handler) {
  733. handler(source);
  734. } else {
  735. reportError("No handler set for: "+
  736. }
  737. }
  738.  
  739. return {
  740. then: function(handleResponse){
  741. handler = handleResponse;
  742. }
  743. };
  744. }
  745. */
  746.