findAndReplaceDOMText-huahuacat

0.4.7

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/454236/1112990/findAndReplaceDOMText-huahuacat.js

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