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

当前为 2019-01-16 提交的版本,查看 最新版本

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