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

As of 2015-08-12. See the latest version.

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