findAndReplaceDOMText v 0.4.6

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.

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/447533/1214813/findAndReplaceDOMText%20v%20046.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,source:1,
  110. // Input elements
  111. input:1, textarea:1, select:1, option:1, optgroup: 1, button:1,
  112. // 自用添加
  113. savdiv:1, avdiv:1,savmagnet:1,
  114. // 添加的其他项目
  115. noscript:1,code:1,footer:1,head:1,nav:1,pre:1,ruby:1
  116. };
  117.  
  118. exposed.NON_CONTIGUOUS_PROSE_ELEMENTS = {
  119.  
  120. // Elements that will not contain prose or block elements where we don't
  121. // want prose to be matches across element borders:
  122.  
  123. // Block Elements
  124. address:1, article:1, aside:1, blockquote:1, dd:1, div:1,
  125. dl:1, fieldset:1, figcaption:1, figure:1, footer:1, form:1, h1:1, h2:1, h3:1,
  126. h4:1, h5:1, h6:1, header:1, hgroup:1, hr:1, main:1, nav:1, noscript:1, ol:1,
  127. output:1, p:1, pre:1, section:1, ul:1,
  128. // Other misc. elements that are not part of continuous inline prose:
  129. br:1, li: 1, summary: 1, dt:1, details:1, rp:1, rt:1, rtc:1,
  130. // Media / Source elements:
  131. script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
  132. // Input elements
  133. input:1, textarea:1, select:1, option:1, optgroup:1, button:1,
  134. // Table related elements:
  135. table:1, tbody:1, thead:1, th:1, tr:1, td:1, caption:1, col:1, tfoot:1, colgroup:1,
  136. // 自用, > v0.10.4 2022-07-25 添加
  137. a:1
  138.  
  139. };
  140.  
  141. exposed.NON_INLINE_PROSE = function(el) {
  142. return hasOwn.call(exposed.NON_CONTIGUOUS_PROSE_ELEMENTS, el.nodeName.toLowerCase());
  143. };
  144.  
  145. // Presets accessed via `options.preset` when calling findAndReplaceDOMText():
  146. exposed.PRESETS = {
  147. prose: {
  148. forceContext: exposed.NON_INLINE_PROSE,
  149. filterElements: function(el) {
  150. // 在链接内的番号进一步筛选
  151. if(el.nodeName.toUpperCase() == "A"){
  152. // 疑似是磁力链接, 略过 magnet:?
  153. if(el.href.match(/magnet:\?/)){
  154. // 如果允许复制, 且不含有特定title
  155. // console.log("链接内含有磁链")
  156. if(window.qxin.CopyMagnet && !el.title.match(/点击复制磁力链接/)){
  157. // var odiv = document.createElement('savmagnet');
  158. // el.outerHTML = "<savmagnet>" + el.outerHTML + "</savmagnet>";
  159. el.title = "点击复制磁力链接";
  160. el.style.textDecoration= "underline #D9B412";
  161. el.addEventListener("click",function(){
  162. GM_setClipboard(el.href);
  163. if(window.qxin.QBit){
  164. // console.log("开始下载")
  165. window.qxin.QBit(el.href);
  166. }
  167. });
  168. }
  169. return false
  170. }
  171. // 排除在链接内的番号, 视为用户名, 排除
  172. if(!window.qxin.includeIDinLinks && el.innerText.search(/^[a-z|A-Z]{2,6}-?\d{2,5}(\.torrent)?$/i)>-1){
  173. // if(el.innerHTML.indexOf("-")<0){ // 导致链接中的 fc2 也会无法识别
  174. // console.log("------------------ 链接内没有横杠: ",el.innerText)
  175. return false
  176. }
  177. if(el.innerText.search(/^[a-z|A-Z]{2,6}\d{2,5}(\.torrent)?$/i)>-1){
  178. // if(el.innerHTML.indexOf("-")<0){ // 导致链接中的 fc2 也会无法识别
  179. // console.log("------------------ 链接内没有横杠: ",el.innerText)
  180. return false
  181. }
  182. }
  183.  
  184. // td是由于图书馆论坛界面的用户名在td中。 位于td内, 且没有横杠的, 排除
  185. if(window.qxin.javlibrary && el.nodeName.toUpperCase() == "TD"){
  186. if(el.innerHTML.search(/^[a-z|A-Z]{2,6}\d{2,5}$/i)>-1){
  187. // if(el.innerHTML.indexOf("-")<0){
  188. // console.log("位于td内, 排除掉: " + el.innerHTML);
  189. return false
  190. }
  191. }
  192.  
  193. // 根据class排除
  194. if(el.classList && el.classList.length
  195. // 对于svg , classname 返回 SVGAnimatedString 的对象导致报错
  196. && typeof(el.className)=="string"
  197. && el.className.match(window.qxin.RE_Exclude_className)
  198. && el.innerText.match(/(?<!\w)[a-z|A-Z]{2,6}[-\s]?\d{2,5}(?!\w)/i)
  199. && el.innerHTML.search("magnet:?")<0){
  200. // console.log("------------------ 特殊class内没有横杠: ",el.className,el)
  201. // console.log(el.innerText)
  202. return false
  203.  
  204. }
  205.  
  206. return !hasOwn.call(exposed.NON_PROSE_ELEMENTS, el.nodeName.toLowerCase());
  207. }
  208. }
  209. };
  210.  
  211. exposed.Finder = Finder;
  212.  
  213. /**
  214. * Finder -- encapsulates logic to find and replace.
  215. */
  216. function Finder(node, options) {
  217.  
  218. var preset = options.preset && exposed.PRESETS[options.preset];
  219.  
  220. options.portionMode = options.portionMode || PORTION_MODE_RETAIN;
  221.  
  222. if (preset) {
  223. for (var i in preset) {
  224. if (hasOwn.call(preset, i) && !hasOwn.call(options, i)) {
  225. options[i] = preset[i];
  226. }
  227. }
  228. }
  229.  
  230. this.node = node;
  231. this.options = options;
  232.  
  233. // Enable match-preparation method to be passed as option:
  234. this.prepMatch = options.prepMatch || this.prepMatch;
  235.  
  236. this.reverts = [];
  237.  
  238. this.matches = this.search();
  239.  
  240. if (this.matches.length) {
  241. this.processMatches();
  242. }
  243.  
  244. }
  245.  
  246. Finder.prototype = {
  247.  
  248. /**
  249. * Searches for all matches that comply with the instance's 'match' option
  250. */
  251. search: function() {
  252.  
  253. var match;
  254. var matchIndex = 0;
  255. var offset = 0;
  256. var regex = this.options.find;
  257. var textAggregation = this.getAggregateText();
  258. var matches = [];
  259. var self = this;
  260.  
  261. regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex;
  262.  
  263. matchAggregation(textAggregation);
  264.  
  265. function matchAggregation(textAggregation) {
  266. for (var i = 0, l = textAggregation.length; i < l; ++i) {
  267.  
  268. var text = textAggregation[i];
  269.  
  270. if (typeof text !== 'string') {
  271. // Deal with nested contexts: (recursive)
  272. matchAggregation(text);
  273. continue;
  274. }
  275.  
  276. if (regex.global) {
  277. while (match = regex.exec(text)) {
  278. matches.push(self.prepMatch(match, matchIndex++, offset));
  279. }
  280. } else {
  281. if (match = text.match(regex)) {
  282. matches.push(self.prepMatch(match, 0, offset));
  283. }
  284. }
  285.  
  286. offset += text.length;
  287. }
  288. }
  289.  
  290. return matches;
  291.  
  292. },
  293.  
  294. /**
  295. * Prepares a single match with useful meta info:
  296. */
  297. prepMatch: function(match, matchIndex, characterOffset) {
  298.  
  299. if (!match[0]) {
  300. throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
  301. }
  302.  
  303. match.endIndex = characterOffset + match.index + match[0].length;
  304. match.startIndex = characterOffset + match.index;
  305. match.index = matchIndex;
  306.  
  307. return match;
  308. },
  309.  
  310. /**
  311. * Gets aggregate text within subject node
  312. */
  313. getAggregateText: function() {
  314.  
  315. var elementFilter = this.options.filterElements;
  316. var forceContext = this.options.forceContext;
  317.  
  318. return getText(this.node);
  319.  
  320. /**
  321. * Gets aggregate text of a node without resorting
  322. * to broken innerText/textContent
  323. */
  324. function getText(node) {
  325.  
  326. if (node.nodeType === Node.TEXT_NODE) {
  327. return [node.data];
  328. }
  329.  
  330. if (elementFilter && !elementFilter(node)) {
  331. return [];
  332. }
  333.  
  334. var txt = [''];
  335. var i = 0;
  336.  
  337. if (node = node.firstChild) do {
  338.  
  339. if (node.nodeType === Node.TEXT_NODE) {
  340. txt[i] += node.data;
  341. continue;
  342. }
  343.  
  344. var innerText = getText(node);
  345.  
  346. if (
  347. forceContext &&
  348. node.nodeType === Node.ELEMENT_NODE &&
  349. (forceContext === true || forceContext(node))
  350. ) {
  351. txt[++i] = innerText;
  352. txt[++i] = '';
  353. } else {
  354. if (typeof innerText[0] === 'string') {
  355. // Bridge nested text-node data so that they're
  356. // not considered their own contexts:
  357. // I.e. ['some', ['thing']] -> ['something']
  358. txt[i] += innerText.shift();
  359. }
  360. if (innerText.length) {
  361. txt[++i] = innerText;
  362. txt[++i] = '';
  363. }
  364. }
  365. } while (node = node.nextSibling);
  366.  
  367. return txt;
  368.  
  369. }
  370.  
  371. },
  372.  
  373. /**
  374. * Steps through the target node, looking for matches, and
  375. * calling replaceFn when a match is found.
  376. */
  377. processMatches: function() {
  378.  
  379. var matches = this.matches;
  380. var node = this.node;
  381. var elementFilter = this.options.filterElements;
  382.  
  383. var startPortion,
  384. endPortion,
  385. innerPortions = [],
  386. curNode = node,
  387. match = matches.shift(),
  388. atIndex = 0, // i.e. nodeAtIndex
  389. matchIndex = 0,
  390. portionIndex = 0,
  391. doAvoidNode,
  392. nodeStack = [node];
  393.  
  394. out: while (true) {
  395.  
  396. if (curNode.nodeType === Node.TEXT_NODE) {
  397.  
  398. if (!endPortion && curNode.length + atIndex >= match.endIndex) {
  399. // We've found the ending
  400. // (Note that, in the case of a single portion, it'll be an
  401. // endPortion, not a startPortion.)
  402. endPortion = {
  403. node: curNode,
  404. index: portionIndex++,
  405. text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex),
  406.  
  407. // If it's the first match (atIndex==0) we should just return 0
  408. indexInMatch: atIndex === 0 ? 0 : atIndex - match.startIndex,
  409.  
  410. indexInNode: match.startIndex - atIndex,
  411. endIndexInNode: match.endIndex - atIndex,
  412. isEnd: true
  413. };
  414.  
  415. } else if (startPortion) {
  416. // Intersecting node
  417. innerPortions.push({
  418. node: curNode,
  419. index: portionIndex++,
  420. text: curNode.data,
  421. indexInMatch: atIndex - match.startIndex,
  422. indexInNode: 0 // always zero for inner-portions
  423. });
  424. }
  425.  
  426. if (!startPortion && curNode.length + atIndex > match.startIndex) {
  427. // We've found the match start
  428. startPortion = {
  429. node: curNode,
  430. index: portionIndex++,
  431. indexInMatch: 0,
  432. indexInNode: match.startIndex - atIndex,
  433. endIndexInNode: match.endIndex - atIndex,
  434. text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex)
  435. };
  436. }
  437.  
  438. atIndex += curNode.data.length;
  439.  
  440. }
  441.  
  442. doAvoidNode = curNode.nodeType === Node.ELEMENT_NODE && elementFilter && !elementFilter(curNode);
  443.  
  444. if (startPortion && endPortion) {
  445.  
  446. curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion);
  447.  
  448. // processMatches has to return the node that replaced the endNode
  449. // and then we step back so we can continue from the end of the
  450. // match:
  451.  
  452. atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode);
  453.  
  454. startPortion = null;
  455. endPortion = null;
  456. innerPortions = [];
  457. match = matches.shift();
  458. portionIndex = 0;
  459. matchIndex++;
  460.  
  461. if (!match) {
  462. break; // no more matches
  463. }
  464.  
  465. } else if (
  466. !doAvoidNode &&
  467. (curNode.firstChild || curNode.nextSibling)
  468. ) {
  469. // Move down or forward:
  470. if (curNode.firstChild) {
  471. nodeStack.push(curNode);
  472. curNode = curNode.firstChild;
  473. } else {
  474. curNode = curNode.nextSibling;
  475. }
  476. continue;
  477. }
  478.  
  479. // Move forward or up:
  480. while (true) {
  481. if (curNode.nextSibling) {
  482. curNode = curNode.nextSibling;
  483. break;
  484. }
  485. curNode = nodeStack.pop();
  486. if (curNode === node) {
  487. break out;
  488. }
  489. }
  490.  
  491. }
  492.  
  493. },
  494.  
  495. /**
  496. * Reverts ... TODO
  497. */
  498. revert: function() {
  499. // Reversion occurs backwards so as to avoid nodes subsequently
  500. // replaced during the matching phase (a forward process):
  501. for (var l = this.reverts.length; l--;) {
  502. this.reverts[l]();
  503. }
  504. this.reverts = [];
  505. },
  506.  
  507. prepareReplacementString: function(string, portion, match) {
  508. var portionMode = this.options.portionMode;
  509. if (
  510. portionMode === PORTION_MODE_FIRST &&
  511. portion.indexInMatch > 0
  512. ) {
  513. return '';
  514. }
  515. string = string.replace(/\$(\d+|&|`|')/g, function($0, t) {
  516. var replacement;
  517. switch(t) {
  518. case '&':
  519. replacement = match[0];
  520. break;
  521. case '`':
  522. replacement = match.input.substring(0, match.startIndex);
  523. break;
  524. case '\'':
  525. replacement = match.input.substring(match.endIndex);
  526. break;
  527. default:
  528. replacement = match[+t] || '';
  529. }
  530. return replacement;
  531. });
  532.  
  533. if (portionMode === PORTION_MODE_FIRST) {
  534. return string;
  535. }
  536.  
  537. if (portion.isEnd) {
  538. return string.substring(portion.indexInMatch);
  539. }
  540.  
  541. return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length);
  542. },
  543.  
  544. getPortionReplacementNode: function(portion, match) {
  545.  
  546. var replacement = this.options.replace || '$&';
  547. var wrapper = this.options.wrap;
  548. var wrapperClass = this.options.wrapClass;
  549.  
  550. if (wrapper && wrapper.nodeType) {
  551. // Wrapper has been provided as a stencil-node for us to clone:
  552. var clone = doc.createElement('div');
  553. clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper);
  554. wrapper = clone.firstChild;
  555. }
  556.  
  557. if (typeof replacement == 'function') {
  558. replacement = replacement(portion, match);
  559. if (replacement && replacement.nodeType) {
  560. return replacement;
  561. }
  562. return doc.createTextNode(String(replacement));
  563. }
  564.  
  565. var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper;
  566.  
  567. if (el && wrapperClass) {
  568. el.className = wrapperClass;
  569. }
  570.  
  571. replacement = doc.createTextNode(
  572. this.prepareReplacementString(
  573. replacement, portion, match
  574. )
  575. );
  576.  
  577. if (!replacement.data) {
  578. return replacement;
  579. }
  580.  
  581. if (!el) {
  582. return replacement;
  583. }
  584.  
  585. el.appendChild(replacement);
  586.  
  587. return el;
  588. },
  589.  
  590. replaceMatch: function(match, startPortion, innerPortions, endPortion) {
  591.  
  592. var matchStartNode = startPortion.node;
  593. var matchEndNode = endPortion.node;
  594.  
  595. var precedingTextNode;
  596. var followingTextNode;
  597.  
  598. if (matchStartNode === matchEndNode) {
  599.  
  600. var node = matchStartNode;
  601.  
  602. if (startPortion.indexInNode > 0) {
  603. // Add `before` text node (before the match)
  604. precedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode));
  605. node.parentNode.insertBefore(precedingTextNode, node);
  606. }
  607.  
  608. // Create the replacement node:
  609. var newNode = this.getPortionReplacementNode(
  610. endPortion,
  611. match
  612. );
  613.  
  614. node.parentNode.insertBefore(newNode, node);
  615.  
  616. if (endPortion.endIndexInNode < node.length) { // ?????
  617. // Add `after` text node (after the match)
  618. followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode));
  619. node.parentNode.insertBefore(followingTextNode, node);
  620. }
  621.  
  622. node.parentNode.removeChild(node);
  623.  
  624. this.reverts.push(function() {
  625. if (precedingTextNode === newNode.previousSibling) {
  626. precedingTextNode.parentNode.removeChild(precedingTextNode);
  627. }
  628. if (followingTextNode === newNode.nextSibling) {
  629. followingTextNode.parentNode.removeChild(followingTextNode);
  630. }
  631. newNode.parentNode.replaceChild(node, newNode);
  632. });
  633.  
  634. return newNode;
  635.  
  636. } else {
  637. // Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order)
  638.  
  639.  
  640. precedingTextNode = doc.createTextNode(
  641. matchStartNode.data.substring(0, startPortion.indexInNode)
  642. );
  643.  
  644. followingTextNode = doc.createTextNode(
  645. matchEndNode.data.substring(endPortion.endIndexInNode)
  646. );
  647.  
  648. var firstNode = this.getPortionReplacementNode(
  649. startPortion,
  650. match
  651. );
  652.  
  653. var innerNodes = [];
  654.  
  655. for (var i = 0, l = innerPortions.length; i < l; ++i) {
  656. var portion = innerPortions[i];
  657. var innerNode = this.getPortionReplacementNode(
  658. portion,
  659. match
  660. );
  661. portion.node.parentNode.replaceChild(innerNode, portion.node);
  662. this.reverts.push((function(portion, innerNode) {
  663. return function() {
  664. innerNode.parentNode.replaceChild(portion.node, innerNode);
  665. };
  666. }(portion, innerNode)));
  667. innerNodes.push(innerNode);
  668. }
  669.  
  670. var lastNode = this.getPortionReplacementNode(
  671. endPortion,
  672. match
  673. );
  674.  
  675. matchStartNode.parentNode.insertBefore(precedingTextNode, matchStartNode);
  676. matchStartNode.parentNode.insertBefore(firstNode, matchStartNode);
  677. matchStartNode.parentNode.removeChild(matchStartNode);
  678.  
  679. matchEndNode.parentNode.insertBefore(lastNode, matchEndNode);
  680. matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode);
  681. matchEndNode.parentNode.removeChild(matchEndNode);
  682.  
  683. this.reverts.push(function() {
  684. precedingTextNode.parentNode.removeChild(precedingTextNode);
  685. firstNode.parentNode.replaceChild(matchStartNode, firstNode);
  686. followingTextNode.parentNode.removeChild(followingTextNode);
  687. lastNode.parentNode.replaceChild(matchEndNode, lastNode);
  688. });
  689.  
  690. return lastNode;
  691. }
  692. }
  693.  
  694. };
  695.  
  696. return exposed;
  697.  
  698. }));