IPFS Hash Linker

Link IPFS hashes to the IPFS gateway

目前为 2015-12-14 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name IPFS Hash Linker
  3. // @namespace IPFS Hash Linker
  4. // @description Link IPFS hashes to the IPFS gateway
  5. // @version 2.1
  6. // @include *
  7. // ==/UserScript==
  8.  
  9. // Tips: 1NT9pxhDcPfsFhnrVoehxiUZ7HCC3gnsn3
  10.  
  11. // Partially based on the "Bitcoin Tool" userscript:
  12. // http://userscripts.org/scripts/show/104381
  13.  
  14. // This is public domain code, free for commercial and non-commercial use.
  15. // License: http://unlicense.org/UNLICENSE
  16.  
  17.  
  18. // We depend on this massive library.
  19. /**
  20. * findAndReplaceDOMText v 0.4.3
  21. * @author James Padolsey http://james.padolsey.com
  22. * @license http://unlicense.org/UNLICENSE
  23. *
  24. * Matches the text of a DOM node against a regular expression
  25. * and replaces each match (or node-separated portions of the match)
  26. * in the specified element.
  27. */
  28. (function (root, factory) {
  29. if (typeof module === 'object' && module.exports) {
  30. // Node/CommonJS
  31. module.exports = factory();
  32. } else if (typeof define === 'function' && define.amd) {
  33. // AMD. Register as an anonymous module.
  34. define(factory);
  35. } else {
  36. // Browser globals
  37. root.findAndReplaceDOMText = factory();
  38. }
  39. }(this, function factory() {
  40.  
  41. var PORTION_MODE_RETAIN = 'retain';
  42. var PORTION_MODE_FIRST = 'first';
  43.  
  44. var doc = document;
  45. var toString = {}.toString;
  46. var hasOwn = {}.hasOwnProperty;
  47.  
  48. function isArray(a) {
  49. return toString.call(a) == '[object Array]';
  50. }
  51.  
  52. function escapeRegExp(s) {
  53. return String(s).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
  54. }
  55.  
  56. function exposed() {
  57. // Try deprecated arg signature first:
  58. return deprecated.apply(null, arguments) || findAndReplaceDOMText.apply(null, arguments);
  59. }
  60.  
  61. function deprecated(regex, node, replacement, captureGroup, elFilter) {
  62. if ((node && !node.nodeType) && arguments.length <= 2) {
  63. return false;
  64. }
  65. var isReplacementFunction = typeof replacement == 'function';
  66.  
  67. if (isReplacementFunction) {
  68. replacement = (function(original) {
  69. return function(portion, match) {
  70. return original(portion.text, match.startIndex);
  71. };
  72. }(replacement));
  73. }
  74.  
  75. // Awkward support for deprecated argument signature (<0.4.0)
  76. var instance = findAndReplaceDOMText(node, {
  77.  
  78. find: regex,
  79.  
  80. wrap: isReplacementFunction ? null : replacement,
  81. replace: isReplacementFunction ? replacement : '$' + (captureGroup || '&'),
  82.  
  83. prepMatch: function(m, mi) {
  84.  
  85. // Support captureGroup (a deprecated feature)
  86.  
  87. if (!m[0]) throw 'findAndReplaceDOMText cannot handle zero-length matches';
  88.  
  89. if (captureGroup > 0) {
  90. var cg = m[captureGroup];
  91. m.index += m[0].indexOf(cg);
  92. m[0] = cg;
  93. }
  94. m.endIndex = m.index + m[0].length;
  95. m.startIndex = m.index;
  96. m.index = mi;
  97.  
  98. return m;
  99. },
  100. filterElements: elFilter
  101. });
  102.  
  103. exposed.revert = function() {
  104. return instance.revert();
  105. };
  106.  
  107. return true;
  108. }
  109.  
  110. /**
  111. * findAndReplaceDOMText
  112. *
  113. * Locates matches and replaces with replacementNode
  114. *
  115. * @param {Node} node Element or Text node to search within
  116. * @param {RegExp} options.find The regular expression to match
  117. * @param {String|Element} [options.wrap] A NodeName, or a Node to clone
  118. * @param {String|Function} [options.replace='$&'] What to replace each match with
  119. * @param {Function} [options.filterElements] A Function to be called to check whether to
  120. * process an element. (returning true = process element,
  121. * returning false = avoid element)
  122. */
  123. function findAndReplaceDOMText(node, options) {
  124. return new Finder(node, options);
  125. }
  126.  
  127. exposed.NON_PROSE_ELEMENTS = {
  128. br:1, hr:1,
  129. // Media / Source elements:
  130. script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
  131. // Input elements
  132. input:1, textarea:1, select:1, option:1, optgroup: 1, button:1
  133. };
  134.  
  135. exposed.NON_CONTIGUOUS_PROSE_ELEMENTS = {
  136.  
  137. // Elements that will not contain prose or block elements where we don't
  138. // want prose to be matches across element borders:
  139.  
  140. // Block Elements
  141. address:1, article:1, aside:1, blockquote:1, dd:1, div:1,
  142. dl:1, fieldset:1, figcaption:1, figure:1, footer:1, form:1, h1:1, h2:1, h3:1,
  143. h4:1, h5:1, h6:1, header:1, hgroup:1, hr:1, main:1, nav:1, noscript:1, ol:1,
  144. output:1, p:1, pre:1, section:1, ul:1,
  145. // Other misc. elements that are not part of continuous inline prose:
  146. br:1, li: 1, summary: 1, dt:1, details:1, rp:1, rt:1, rtc:1,
  147. // Media / Source elements:
  148. script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
  149. // Input elements
  150. input:1, textarea:1, select:1, option:1, optgroup: 1, button:1,
  151. // Table related elements:
  152. table:1, tbody:1, thead:1, th:1, tr:1, td:1, caption:1, col:1, tfoot:1, colgroup:1
  153.  
  154. };
  155.  
  156. exposed.NON_INLINE_PROSE = function(el) {
  157. return hasOwn.call(exposed.NON_CONTIGUOUS_PROSE_ELEMENTS, el.nodeName.toLowerCase());
  158. };
  159.  
  160. // Presets accessed via `options.preset` when calling findAndReplaceDOMText():
  161. exposed.PRESETS = {
  162. prose: {
  163. forceContext: exposed.NON_INLINE_PROSE,
  164. filterElements: function(el) {
  165. return !hasOwn.call(exposed.NON_PROSE_ELEMENTS, el.nodeName.toLowerCase());
  166. }
  167. }
  168. };
  169.  
  170. exposed.Finder = Finder;
  171.  
  172. /**
  173. * Finder -- encapsulates logic to find and replace.
  174. */
  175. function Finder(node, options) {
  176.  
  177. var preset = options.preset && exposed.PRESETS[options.preset];
  178.  
  179. options.portionMode = options.portionMode || PORTION_MODE_RETAIN;
  180.  
  181. if (preset) {
  182. for (var i in preset) {
  183. if (hasOwn.call(preset, i) && !hasOwn.call(options, i)) {
  184. options[i] = preset[i];
  185. }
  186. }
  187. }
  188.  
  189. this.node = node;
  190. this.options = options;
  191.  
  192. // ENable match-preparation method to be passed as option:
  193. this.prepMatch = options.prepMatch || this.prepMatch;
  194.  
  195. this.reverts = [];
  196.  
  197. this.matches = this.search();
  198.  
  199. if (this.matches.length) {
  200. this.processMatches();
  201. }
  202.  
  203. }
  204.  
  205. Finder.prototype = {
  206.  
  207. /**
  208. * Searches for all matches that comply with the instance's 'match' option
  209. */
  210. search: function() {
  211.  
  212. var match;
  213. var matchIndex = 0;
  214. var offset = 0;
  215. var regex = this.options.find;
  216. var textAggregation = this.getAggregateText();
  217. var matches = [];
  218. var self = this;
  219.  
  220. regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex;
  221.  
  222. matchAggregation(textAggregation);
  223.  
  224. function matchAggregation(textAggregation) {
  225. for (var i = 0, l = textAggregation.length; i < l; ++i) {
  226.  
  227. var text = textAggregation[i];
  228.  
  229. if (typeof text !== 'string') {
  230. // Deal with nested contexts: (recursive)
  231. matchAggregation(text);
  232. continue;
  233. }
  234.  
  235. if (regex.global) {
  236. while (match = regex.exec(text)) {
  237. matches.push(self.prepMatch(match, matchIndex++, offset));
  238. }
  239. } else {
  240. if (match = text.match(regex)) {
  241. matches.push(self.prepMatch(match, 0, offset));
  242. }
  243. }
  244.  
  245. offset += text.length;
  246. }
  247. }
  248.  
  249. return matches;
  250.  
  251. },
  252.  
  253. /**
  254. * Prepares a single match with useful meta info:
  255. */
  256. prepMatch: function(match, matchIndex, characterOffset) {
  257.  
  258. if (!match[0]) {
  259. throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
  260. }
  261. match.endIndex = characterOffset + match.index + match[0].length;
  262. match.startIndex = characterOffset + match.index;
  263. match.index = matchIndex;
  264.  
  265. return match;
  266. },
  267.  
  268. /**
  269. * Gets aggregate text within subject node
  270. */
  271. getAggregateText: function() {
  272.  
  273. var elementFilter = this.options.filterElements;
  274. var forceContext = this.options.forceContext;
  275.  
  276. return getText(this.node);
  277.  
  278. /**
  279. * Gets aggregate text of a node without resorting
  280. * to broken innerText/textContent
  281. */
  282. function getText(node, txt) {
  283.  
  284. if (node.nodeType === 3) {
  285. return [node.data];
  286. }
  287.  
  288. if (elementFilter && !elementFilter(node)) {
  289. return [];
  290. }
  291.  
  292. var txt = [''];
  293. var i = 0;
  294.  
  295. if (node = node.firstChild) do {
  296.  
  297. if (node.nodeType === 3) {
  298. txt[i] += node.data;
  299. continue;
  300. }
  301.  
  302. var innerText = getText(node);
  303.  
  304. if (
  305. forceContext &&
  306. node.nodeType === 1 &&
  307. (forceContext === true || forceContext(node))
  308. ) {
  309. txt[++i] = innerText;
  310. txt[++i] = '';
  311. } else {
  312. if (typeof innerText[0] === 'string') {
  313. // Bridge nested text-node data so that they're
  314. // not considered their own contexts:
  315. // I.e. ['some', ['thing']] -> ['something']
  316. txt[i] += innerText.shift();
  317. }
  318. if (innerText.length) {
  319. txt[++i] = innerText;
  320. txt[++i] = '';
  321. }
  322. }
  323. } while (node = node.nextSibling);
  324.  
  325. return txt;
  326.  
  327. }
  328. },
  329.  
  330. /**
  331. * Steps through the target node, looking for matches, and
  332. * calling replaceFn when a match is found.
  333. */
  334. processMatches: function() {
  335.  
  336. var matches = this.matches;
  337. var node = this.node;
  338. var elementFilter = this.options.filterElements;
  339.  
  340. var startPortion,
  341. endPortion,
  342. innerPortions = [],
  343. curNode = node,
  344. match = matches.shift(),
  345. atIndex = 0, // i.e. nodeAtIndex
  346. matchIndex = 0,
  347. portionIndex = 0,
  348. doAvoidNode,
  349. nodeStack = [node];
  350.  
  351. out: while (true) {
  352.  
  353. if (curNode.nodeType === 3) {
  354.  
  355. if (!endPortion && curNode.length + atIndex >= match.endIndex) {
  356.  
  357. // We've found the ending
  358. endPortion = {
  359. node: curNode,
  360. index: portionIndex++,
  361. text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex),
  362. indexInMatch: atIndex - match.startIndex,
  363. indexInNode: match.startIndex - atIndex, // always zero for end-portions
  364. endIndexInNode: match.endIndex - atIndex,
  365. isEnd: true
  366. };
  367.  
  368. } else if (startPortion) {
  369. // Intersecting node
  370. innerPortions.push({
  371. node: curNode,
  372. index: portionIndex++,
  373. text: curNode.data,
  374. indexInMatch: atIndex - match.startIndex,
  375. indexInNode: 0 // always zero for inner-portions
  376. });
  377. }
  378.  
  379. if (!startPortion && curNode.length + atIndex > match.startIndex) {
  380. // We've found the match start
  381. startPortion = {
  382. node: curNode,
  383. index: portionIndex++,
  384. indexInMatch: 0,
  385. indexInNode: match.startIndex - atIndex,
  386. endIndexInNode: match.endIndex - atIndex,
  387. text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex)
  388. };
  389. }
  390.  
  391. atIndex += curNode.data.length;
  392.  
  393. }
  394.  
  395. doAvoidNode = curNode.nodeType === 1 && elementFilter && !elementFilter(curNode);
  396.  
  397. if (startPortion && endPortion) {
  398.  
  399. curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion);
  400.  
  401. // processMatches has to return the node that replaced the endNode
  402. // and then we step back so we can continue from the end of the
  403. // match:
  404.  
  405. atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode);
  406.  
  407. startPortion = null;
  408. endPortion = null;
  409. innerPortions = [];
  410. match = matches.shift();
  411. portionIndex = 0;
  412. matchIndex++;
  413.  
  414. if (!match) {
  415. break; // no more matches
  416. }
  417.  
  418. } else if (
  419. !doAvoidNode &&
  420. (curNode.firstChild || curNode.nextSibling)
  421. ) {
  422. // Move down or forward:
  423. if (curNode.firstChild) {
  424. nodeStack.push(curNode);
  425. curNode = curNode.firstChild;
  426. } else {
  427. curNode = curNode.nextSibling;
  428. }
  429. continue;
  430. }
  431.  
  432. // Move forward or up:
  433. while (true) {
  434. if (curNode.nextSibling) {
  435. curNode = curNode.nextSibling;
  436. break;
  437. }
  438. curNode = nodeStack.pop();
  439. if (curNode === node) {
  440. break out;
  441. }
  442. }
  443.  
  444. }
  445.  
  446. },
  447.  
  448. /**
  449. * Reverts ... TODO
  450. */
  451. revert: function() {
  452. // Reversion occurs backwards so as to avoid nodes subsequently
  453. // replaced during the matching phase (a forward process):
  454. for (var l = this.reverts.length; l--;) {
  455. this.reverts[l]();
  456. }
  457. this.reverts = [];
  458. },
  459.  
  460. prepareReplacementString: function(string, portion, match, matchIndex) {
  461. var portionMode = this.options.portionMode;
  462. if (
  463. portionMode === PORTION_MODE_FIRST &&
  464. portion.indexInMatch > 0
  465. ) {
  466. return '';
  467. }
  468. string = string.replace(/\$(\d+|&|`|')/g, function($0, t) {
  469. var replacement;
  470. switch(t) {
  471. case '&':
  472. replacement = match[0];
  473. break;
  474. case '`':
  475. replacement = match.input.substring(0, match.startIndex);
  476. break;
  477. case '\'':
  478. replacement = match.input.substring(match.endIndex);
  479. break;
  480. default:
  481. replacement = match[+t];
  482. }
  483. return replacement;
  484. });
  485.  
  486. if (portionMode === PORTION_MODE_FIRST) {
  487. return string;
  488. }
  489.  
  490. if (portion.isEnd) {
  491. return string.substring(portion.indexInMatch);
  492. }
  493.  
  494. return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length);
  495. },
  496.  
  497. getPortionReplacementNode: function(portion, match, matchIndex) {
  498.  
  499. var replacement = this.options.replace || '$&';
  500. var wrapper = this.options.wrap;
  501.  
  502. if (wrapper && wrapper.nodeType) {
  503. // Wrapper has been provided as a stencil-node for us to clone:
  504. var clone = doc.createElement('div');
  505. clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper);
  506. wrapper = clone.firstChild;
  507. }
  508.  
  509. if (typeof replacement == 'function') {
  510. replacement = replacement(portion, match, matchIndex);
  511. if (replacement && replacement.nodeType) {
  512. return replacement;
  513. }
  514. return doc.createTextNode(String(replacement));
  515. }
  516.  
  517. var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper;
  518.  
  519. replacement = doc.createTextNode(
  520. this.prepareReplacementString(
  521. replacement, portion, match, matchIndex
  522. )
  523. );
  524.  
  525. if (!replacement.data) {
  526. return replacement;
  527. }
  528.  
  529. if (!el) {
  530. return replacement;
  531. }
  532.  
  533. el.appendChild(replacement);
  534.  
  535. return el;
  536. },
  537.  
  538. replaceMatch: function(match, startPortion, innerPortions, endPortion) {
  539.  
  540. var matchStartNode = startPortion.node;
  541. var matchEndNode = endPortion.node;
  542.  
  543. var preceedingTextNode;
  544. var followingTextNode;
  545.  
  546. if (matchStartNode === matchEndNode) {
  547.  
  548. var node = matchStartNode;
  549.  
  550. if (startPortion.indexInNode > 0) {
  551. // Add `before` text node (before the match)
  552. preceedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode));
  553. node.parentNode.insertBefore(preceedingTextNode, node);
  554. }
  555.  
  556. // Create the replacement node:
  557. var newNode = this.getPortionReplacementNode(
  558. endPortion,
  559. match
  560. );
  561.  
  562. node.parentNode.insertBefore(newNode, node);
  563.  
  564. if (endPortion.endIndexInNode < node.length) { // ?????
  565. // Add `after` text node (after the match)
  566. followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode));
  567. node.parentNode.insertBefore(followingTextNode, node);
  568. }
  569.  
  570. node.parentNode.removeChild(node);
  571.  
  572. this.reverts.push(function() {
  573. if (preceedingTextNode === newNode.previousSibling) {
  574. preceedingTextNode.parentNode.removeChild(preceedingTextNode);
  575. }
  576. if (followingTextNode === newNode.nextSibling) {
  577. followingTextNode.parentNode.removeChild(followingTextNode);
  578. }
  579. newNode.parentNode.replaceChild(node, newNode);
  580. });
  581.  
  582. return newNode;
  583.  
  584. } else {
  585. // Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order)
  586.  
  587.  
  588. preceedingTextNode = doc.createTextNode(
  589. matchStartNode.data.substring(0, startPortion.indexInNode)
  590. );
  591.  
  592. followingTextNode = doc.createTextNode(
  593. matchEndNode.data.substring(endPortion.endIndexInNode)
  594. );
  595.  
  596. var firstNode = this.getPortionReplacementNode(
  597. startPortion,
  598. match
  599. );
  600.  
  601. var innerNodes = [];
  602.  
  603. for (var i = 0, l = innerPortions.length; i < l; ++i) {
  604. var portion = innerPortions[i];
  605. var innerNode = this.getPortionReplacementNode(
  606. portion,
  607. match
  608. );
  609. portion.node.parentNode.replaceChild(innerNode, portion.node);
  610. this.reverts.push((function(portion, innerNode) {
  611. return function() {
  612. innerNode.parentNode.replaceChild(portion.node, innerNode);
  613. };
  614. }(portion, innerNode)));
  615. innerNodes.push(innerNode);
  616. }
  617.  
  618. var lastNode = this.getPortionReplacementNode(
  619. endPortion,
  620. match
  621. );
  622.  
  623. matchStartNode.parentNode.insertBefore(preceedingTextNode, matchStartNode);
  624. matchStartNode.parentNode.insertBefore(firstNode, matchStartNode);
  625. matchStartNode.parentNode.removeChild(matchStartNode);
  626.  
  627. matchEndNode.parentNode.insertBefore(lastNode, matchEndNode);
  628. matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode);
  629. matchEndNode.parentNode.removeChild(matchEndNode);
  630.  
  631. this.reverts.push(function() {
  632. preceedingTextNode.parentNode.removeChild(preceedingTextNode);
  633. firstNode.parentNode.replaceChild(matchStartNode, firstNode);
  634. followingTextNode.parentNode.removeChild(followingTextNode);
  635. lastNode.parentNode.replaceChild(matchEndNode, lastNode);
  636. });
  637.  
  638. return lastNode;
  639. }
  640. }
  641.  
  642. };
  643.  
  644. return exposed;
  645.  
  646. }));
  647.  
  648. // Actual userscript code
  649.  
  650. var bad_elements = {
  651.  
  652. // Elements we shouldn't try to link hashes in
  653. // Preformatted/already linked elements
  654. a: 1, pre:1, code:1,
  655. // Media / Source elements:
  656. script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
  657. // Input elements
  658. input:1, textarea:1, select:1, option:1, optgroup: 1, button:1
  659. };
  660.  
  661. findAndReplaceDOMText(document.documentElement, {
  662. find: /(\/?ipfs\/|\/?ipns\/)?(Qm[A-HJ-NP-Za-km-z1-9]{44,45})/g,
  663. replace: function(portion, match) {
  664. var path = match[1];
  665. var hash = match[2];
  666.  
  667. if(typeof path == "undefined") {
  668. // Sometimes there's no leading /ipfs/
  669. path = "/ipfs/";
  670. }
  671. var link = document.createElement('a');
  672. link.setAttribute("href", "https://gateway.ipfs.io" + path + hash);
  673. // Don't just set inner text; it may not work if there's no text node alreay.
  674. var content = document.createTextNode(path + hash);
  675. link.appendChild(content);
  676. return link;
  677. },
  678. forceContext: function(el) {
  679. return true;
  680. },
  681. filterElements: function(el) {
  682. return !bad_elements.hasOwnProperty(el.nodeName.toLowerCase());
  683. }
  684. });