Greasy Fork 支持简体中文。

findAndReplaceDOMText v 0.4.0

Matches the text of a DOM node against a regular expression and replaces each match (or node-separated portions of the match) in the specified element.

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/423347/911450/findAndReplaceDOMText%20v%20040.js

  1. /**
  2. * findAndReplaceDOMText v 0.4.0
  3. * @author James Padolsey http://james.padolsey.com
  4. * @license http://unlicense.org/UNLICENSE
  5. *
  6. * Matches the text of a DOM node against a regular expression
  7. * and replaces each match (or node-separated portions of the match)
  8. * in the specified element.
  9. */
  10. window.findAndReplaceDOMText = (function() {
  11.  
  12. var PORTION_MODE_RETAIN = 'retain';
  13. var PORTION_MODE_FIRST = 'first';
  14.  
  15. var doc = document;
  16. var toString = {}.toString;
  17.  
  18. function isArray(a) {
  19. return toString.call(a) == '[object Array]';
  20. }
  21.  
  22. function escapeRegExp(s) {
  23. return String(s).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
  24. }
  25.  
  26. function exposed() {
  27. // Try deprecated arg signature first:
  28. return deprecated.apply(null, arguments) || findAndReplaceDOMText.apply(null, arguments);
  29. }
  30.  
  31. function deprecated(regex, node, replacement, captureGroup, elFilter) {
  32. if ((node && !node.nodeType) && arguments.length <= 2) {
  33. return false;
  34. }
  35. var isReplacementFunction = typeof replacement == 'function';
  36.  
  37. if (isReplacementFunction) {
  38. replacement = (function(original) {
  39. return function(portion, match) {
  40. return original(portion.text, match.startIndex);
  41. };
  42. }(replacement));
  43. }
  44.  
  45. // Awkward support for deprecated argument signature (<0.4.0)
  46. var instance = findAndReplaceDOMText(node, {
  47.  
  48. find: regex,
  49.  
  50. wrap: isReplacementFunction ? null : replacement,
  51. replace: isReplacementFunction ? replacement : '$' + (captureGroup || '&'),
  52.  
  53. prepMatch: function(m, mi) {
  54.  
  55. // Support captureGroup (a deprecated feature)
  56.  
  57. if (!m[0]) throw 'findAndReplaceDOMText cannot handle zero-length matches';
  58.  
  59. if (captureGroup > 0) {
  60. var cg = m[captureGroup];
  61. m.index += m[0].indexOf(cg);
  62. m[0] = cg;
  63. }
  64. m.endIndex = m.index + m[0].length;
  65. m.startIndex = m.index;
  66. m.index = mi;
  67.  
  68. return m;
  69. },
  70. filterElements: elFilter
  71. });
  72.  
  73. exposed.revert = function() {
  74. return instance.revert();
  75. };
  76.  
  77. return true;
  78. }
  79.  
  80. /**
  81. * findAndReplaceDOMText
  82. *
  83. * Locates matches and replaces with replacementNode
  84. *
  85. * @param {Node} node Element or Text node to search within
  86. * @param {RegExp} options.find The regular expression to match
  87. * @param {String|Element} [options.wrap] A NodeName, or a Node to clone
  88. * @param {String|Function} [options.replace='$&'] What to replace each match with
  89. * @param {Function} [options.filterElements] A Function to be called to check whether to
  90. * process an element. (returning true = process element,
  91. * returning false = avoid element)
  92. */
  93. function findAndReplaceDOMText(node, options) {
  94. return new Finder(node, options);
  95. }
  96.  
  97. exposed.Finder = Finder;
  98.  
  99. /**
  100. * Finder -- encapsulates logic to find and replace.
  101. */
  102. function Finder(node, options) {
  103.  
  104. options.portionMode = options.portionMode || PORTION_MODE_RETAIN;
  105.  
  106. this.node = node;
  107. this.options = options;
  108.  
  109. // ENable match-preparation method to be passed as option:
  110. this.prepMatch = options.prepMatch || this.prepMatch;
  111.  
  112. this.reverts = [];
  113.  
  114. this.matches = this.search();
  115.  
  116. if (this.matches.length) {
  117. this.processMatches();
  118. }
  119.  
  120. }
  121.  
  122. Finder.prototype = {
  123.  
  124. /**
  125. * Searches for all matches that comply with the instance's 'match' option
  126. */
  127. search: function() {
  128.  
  129. var match;
  130. var matchIndex = 0;
  131. var regex = this.options.find;
  132. var text = this.getAggregateText();
  133. var matches = [];
  134.  
  135. regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex;
  136.  
  137. if (regex.global) {
  138. while (match = regex.exec(text)) {
  139. matches.push(this.prepMatch(match, matchIndex++));
  140. }
  141. } else {
  142. if (match = text.match(regex)) {
  143. matches.push(this.prepMatch(match, 0));
  144. }
  145. }
  146.  
  147. return matches;
  148.  
  149. },
  150.  
  151. /**
  152. * Prepares a single match with useful meta info:
  153. */
  154. prepMatch: function(match, matchIndex) {
  155.  
  156. if (!match[0]) {
  157. throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
  158. }
  159. match.endIndex = match.index + match[0].length;
  160. match.startIndex = match.index;
  161. match.index = matchIndex;
  162.  
  163. return match;
  164. },
  165.  
  166. /**
  167. * Gets aggregate text within subject node
  168. */
  169. getAggregateText: function() {
  170.  
  171. var elementFilter = this.options.filterElements;
  172.  
  173. return getText(this.node);
  174.  
  175. /**
  176. * Gets aggregate text of a node without resorting
  177. * to broken innerText/textContent
  178. */
  179. function getText(node) {
  180.  
  181. if (node.nodeType === 3) {
  182. return node.data;
  183. }
  184.  
  185. if (elementFilter && !elementFilter(node)) {
  186. return '';
  187. }
  188.  
  189. var txt = '';
  190.  
  191. if (node = node.firstChild) do {
  192. txt += getText(node);
  193. } while (node = node.nextSibling);
  194.  
  195. return txt;
  196.  
  197. }
  198.  
  199. },
  200.  
  201. /**
  202. * Steps through the target node, looking for matches, and
  203. * calling replaceFn when a match is found.
  204. */
  205. processMatches: function() {
  206.  
  207. var matches = this.matches;
  208. var node = this.node;
  209. var elementFilter = this.options.filterElements;
  210.  
  211. var startPortion,
  212. endPortion,
  213. innerPortions = [],
  214. curNode = node,
  215. match = matches.shift(),
  216. atIndex = 0, // i.e. nodeAtIndex
  217. matchIndex = 0,
  218. portionIndex = 0,
  219. doAvoidNode;
  220.  
  221. out: while (true) {
  222.  
  223. if (curNode.nodeType === 3) {
  224.  
  225. if (!endPortion && curNode.length + atIndex >= match.endIndex) {
  226.  
  227. // We've found the ending
  228. endPortion = {
  229. node: curNode,
  230. index: portionIndex++,
  231. text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex),
  232. indexInMatch: atIndex - match.startIndex,
  233. indexInNode: match.startIndex - atIndex, // always zero for end-portions
  234. endIndexInNode: match.endIndex - atIndex,
  235. isEnd: true
  236. };
  237.  
  238. } else if (startPortion) {
  239. // Intersecting node
  240. innerPortions.push({
  241. node: curNode,
  242. index: portionIndex++,
  243. text: curNode.data,
  244. indexInMatch: atIndex - match.startIndex,
  245. indexInNode: 0 // always zero for inner-portions
  246. });
  247. }
  248.  
  249. if (!startPortion && curNode.length + atIndex > match.startIndex) {
  250. // We've found the match start
  251. startPortion = {
  252. node: curNode,
  253. index: portionIndex++,
  254. indexInMatch: 0,
  255. indexInNode: match.startIndex - atIndex,
  256. endIndexInNode: match.endIndex - atIndex,
  257. text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex)
  258. };
  259. }
  260.  
  261. atIndex += curNode.data.length;
  262.  
  263. }
  264.  
  265. doAvoidNode = curNode.nodeType === 1 && elementFilter && !elementFilter(curNode);
  266.  
  267. if (startPortion && endPortion) {
  268.  
  269. curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion);
  270.  
  271. // processMatches has to return the node that replaced the endNode
  272. // and then we step back so we can continue from the end of the
  273. // match:
  274.  
  275. atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode);
  276.  
  277. startPortion = null;
  278. endPortion = null;
  279. innerPortions = [];
  280. match = matches.shift();
  281. portionIndex = 0;
  282. matchIndex++;
  283.  
  284. if (!match) {
  285. break; // no more matches
  286. }
  287.  
  288. } else if (
  289. !doAvoidNode &&
  290. (curNode.firstChild || curNode.nextSibling)
  291. ) {
  292. // Move down or forward:
  293. curNode = curNode.firstChild || curNode.nextSibling;
  294. continue;
  295. }
  296.  
  297. // Move forward or up:
  298. while (true) {
  299. if (curNode.nextSibling) {
  300. curNode = curNode.nextSibling;
  301. break;
  302. } else if (curNode.parentNode !== node) {
  303. curNode = curNode.parentNode;
  304. } else {
  305. break out;
  306. }
  307. }
  308.  
  309. }
  310.  
  311. },
  312.  
  313. /**
  314. * Reverts ... TODO
  315. */
  316. revert: function() {
  317. // Reversion occurs backwards so as to avoid nodes subsequently
  318. // replaced during the matching phase (a forward process):
  319. for (var l = this.reverts.length; l--;) {
  320. this.reverts[l]();
  321. }
  322. this.reverts = [];
  323. },
  324.  
  325. prepareReplacementString: function(string, portion, match, matchIndex) {
  326. var portionMode = this.options.portionMode;
  327. if (
  328. portionMode === PORTION_MODE_FIRST &&
  329. portion.indexInMatch > 0
  330. ) {
  331. return '';
  332. }
  333. string = string.replace(/\$(\d+|&|`|')/g, function($0, t) {
  334. var replacement;
  335. switch(t) {
  336. case '&':
  337. replacement = match[0];
  338. break;
  339. case '`':
  340. replacement = match.input.substring(0, match.startIndex);
  341. break;
  342. case '\'':
  343. replacement = match.input.substring(match.endIndex);
  344. break;
  345. default:
  346. replacement = match[+t];
  347. }
  348. return replacement;
  349. });
  350.  
  351. if (portionMode === PORTION_MODE_FIRST) {
  352. return string;
  353. }
  354.  
  355. if (portion.isEnd) {
  356. return string.substring(portion.indexInMatch);
  357. }
  358.  
  359. return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length);
  360. },
  361.  
  362. getPortionReplacementNode: function(portion, match, matchIndex) {
  363.  
  364. var replacement = this.options.replace || '$&';
  365. var wrapper = this.options.wrap;
  366.  
  367. if (wrapper && wrapper.nodeType) {
  368. // Wrapper has been provided as a stencil-node for us to clone:
  369. var clone = doc.createElement('div');
  370. clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper);
  371. wrapper = clone.firstChild;
  372. }
  373.  
  374. if (typeof replacement == 'function') {
  375. replacement = replacement(portion, match, matchIndex);
  376. if (replacement && replacement.nodeType) {
  377. return replacement;
  378. }
  379. return doc.createTextNode(String(replacement));
  380. }
  381.  
  382. var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper;
  383.  
  384. replacement = doc.createTextNode(
  385. this.prepareReplacementString(
  386. replacement, portion, match, matchIndex
  387. )
  388. );
  389.  
  390. if (!el) {
  391. return replacement;
  392. }
  393.  
  394. el.appendChild(replacement);
  395.  
  396. return el;
  397. },
  398.  
  399. replaceMatch: function(match, startPortion, innerPortions, endPortion) {
  400.  
  401. var matchStartNode = startPortion.node;
  402. var matchEndNode = endPortion.node;
  403.  
  404. var preceedingTextNode;
  405. var followingTextNode;
  406.  
  407. if (matchStartNode === matchEndNode) {
  408.  
  409. var node = matchStartNode;
  410.  
  411. if (startPortion.indexInNode > 0) {
  412. // Add `before` text node (before the match)
  413. preceedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode));
  414. node.parentNode.insertBefore(preceedingTextNode, node);
  415. }
  416.  
  417. // Create the replacement node:
  418. var newNode = this.getPortionReplacementNode(
  419. endPortion,
  420. match
  421. );
  422.  
  423. node.parentNode.insertBefore(newNode, node);
  424.  
  425. if (endPortion.endIndexInNode < node.length) { // ?????
  426. // Add `after` text node (after the match)
  427. followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode));
  428. node.parentNode.insertBefore(followingTextNode, node);
  429. }
  430.  
  431. node.parentNode.removeChild(node);
  432.  
  433. this.reverts.push(function() {
  434. if (preceedingTextNode === newNode.previousSibling) {
  435. preceedingTextNode.parentNode.removeChild(preceedingTextNode);
  436. }
  437. if (followingTextNode === newNode.nextSibling) {
  438. followingTextNode.parentNode.removeChild(followingTextNode);
  439. }
  440. newNode.parentNode.replaceChild(node, newNode);
  441. });
  442.  
  443. return newNode;
  444.  
  445. } else {
  446. // Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order)
  447.  
  448.  
  449. preceedingTextNode = doc.createTextNode(
  450. matchStartNode.data.substring(0, startPortion.indexInNode)
  451. );
  452.  
  453. followingTextNode = doc.createTextNode(
  454. matchEndNode.data.substring(endPortion.endIndexInNode)
  455. );
  456.  
  457. var firstNode = this.getPortionReplacementNode(
  458. startPortion,
  459. match
  460. );
  461.  
  462. var innerNodes = [];
  463.  
  464. for (var i = 0, l = innerPortions.length; i < l; ++i) {
  465. var portion = innerPortions[i];
  466. var innerNode = this.getPortionReplacementNode(
  467. portion,
  468. match
  469. );
  470. portion.node.parentNode.replaceChild(innerNode, portion.node);
  471. this.reverts.push((function(portion, innerNode) {
  472. return function() {
  473. innerNode.parentNode.replaceChild(portion.node, innerNode);
  474. };
  475. }(portion, innerNode)));
  476. innerNodes.push(innerNode);
  477. }
  478.  
  479. var lastNode = this.getPortionReplacementNode(
  480. endPortion,
  481. match
  482. );
  483.  
  484. matchStartNode.parentNode.insertBefore(preceedingTextNode, matchStartNode);
  485. matchStartNode.parentNode.insertBefore(firstNode, matchStartNode);
  486. matchStartNode.parentNode.removeChild(matchStartNode);
  487.  
  488. matchEndNode.parentNode.insertBefore(lastNode, matchEndNode);
  489. matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode);
  490. matchEndNode.parentNode.removeChild(matchEndNode);
  491.  
  492. this.reverts.push(function() {
  493. preceedingTextNode.parentNode.removeChild(preceedingTextNode);
  494. firstNode.parentNode.replaceChild(matchStartNode, firstNode);
  495. followingTextNode.parentNode.removeChild(followingTextNode);
  496. lastNode.parentNode.replaceChild(matchEndNode, lastNode);
  497. });
  498.  
  499. return lastNode;
  500. }
  501. }
  502.  
  503. };
  504.  
  505. return exposed;
  506.  
  507. }());