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.

目前为 2022-10-23 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/447533/1108523/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(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. return false
  189. }
  190. }
  191.  
  192. // 根据class排除
  193. if(el.classList && el.classList.length
  194. // 对于svg , classname 返回 SVGAnimatedString 的对象导致报错
  195. && typeof(el.className)=="string"
  196. && el.className.match(window.qxin.RE_Exclude_className)
  197. && el.innerText.match(/(?<!\w)[a-z|A-Z]{2,6}[-\s]?\d{2,5}(?!\w)/i)
  198. && el.innerHTML.search("magnet:?")<0){
  199. // console.log("------------------ 特殊class内没有横杠: ",el.className)
  200. // console.log(el.innerText)
  201. return false
  202.  
  203. }
  204.  
  205. return !hasOwn.call(exposed.NON_PROSE_ELEMENTS, el.nodeName.toLowerCase());
  206. }
  207. }
  208. };
  209.  
  210. exposed.Finder = Finder;
  211.  
  212. /**
  213. * Finder -- encapsulates logic to find and replace.
  214. */
  215. function Finder(node, options) {
  216.  
  217. var preset = options.preset && exposed.PRESETS[options.preset];
  218.  
  219. options.portionMode = options.portionMode || PORTION_MODE_RETAIN;
  220.  
  221. if (preset) {
  222. for (var i in preset) {
  223. if (hasOwn.call(preset, i) && !hasOwn.call(options, i)) {
  224. options[i] = preset[i];
  225. }
  226. }
  227. }
  228.  
  229. this.node = node;
  230. this.options = options;
  231.  
  232. // Enable match-preparation method to be passed as option:
  233. this.prepMatch = options.prepMatch || this.prepMatch;
  234.  
  235. this.reverts = [];
  236.  
  237. this.matches = this.search();
  238.  
  239. if (this.matches.length) {
  240. this.processMatches();
  241. }
  242.  
  243. }
  244.  
  245. Finder.prototype = {
  246.  
  247. /**
  248. * Searches for all matches that comply with the instance's 'match' option
  249. */
  250. search: function() {
  251.  
  252. var match;
  253. var matchIndex = 0;
  254. var offset = 0;
  255. var regex = this.options.find;
  256. var textAggregation = this.getAggregateText();
  257. var matches = [];
  258. var self = this;
  259.  
  260. regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex;
  261.  
  262. matchAggregation(textAggregation);
  263.  
  264. function matchAggregation(textAggregation) {
  265. for (var i = 0, l = textAggregation.length; i < l; ++i) {
  266.  
  267. var text = textAggregation[i];
  268.  
  269. if (typeof text !== 'string') {
  270. // Deal with nested contexts: (recursive)
  271. matchAggregation(text);
  272. continue;
  273. }
  274.  
  275. if (regex.global) {
  276. while (match = regex.exec(text)) {
  277. matches.push(self.prepMatch(match, matchIndex++, offset));
  278. }
  279. } else {
  280. if (match = text.match(regex)) {
  281. matches.push(self.prepMatch(match, 0, offset));
  282. }
  283. }
  284.  
  285. offset += text.length;
  286. }
  287. }
  288.  
  289. return matches;
  290.  
  291. },
  292.  
  293. /**
  294. * Prepares a single match with useful meta info:
  295. */
  296. prepMatch: function(match, matchIndex, characterOffset) {
  297.  
  298. if (!match[0]) {
  299. throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
  300. }
  301.  
  302. match.endIndex = characterOffset + match.index + match[0].length;
  303. match.startIndex = characterOffset + match.index;
  304. match.index = matchIndex;
  305.  
  306. return match;
  307. },
  308.  
  309. /**
  310. * Gets aggregate text within subject node
  311. */
  312. getAggregateText: function() {
  313.  
  314. var elementFilter = this.options.filterElements;
  315. var forceContext = this.options.forceContext;
  316.  
  317. return getText(this.node);
  318.  
  319. /**
  320. * Gets aggregate text of a node without resorting
  321. * to broken innerText/textContent
  322. */
  323. function getText(node) {
  324.  
  325. if (node.nodeType === Node.TEXT_NODE) {
  326. return [node.data];
  327. }
  328.  
  329. if (elementFilter && !elementFilter(node)) {
  330. return [];
  331. }
  332.  
  333. var txt = [''];
  334. var i = 0;
  335.  
  336. if (node = node.firstChild) do {
  337.  
  338. if (node.nodeType === Node.TEXT_NODE) {
  339. txt[i] += node.data;
  340. continue;
  341. }
  342.  
  343. var innerText = getText(node);
  344.  
  345. if (
  346. forceContext &&
  347. node.nodeType === Node.ELEMENT_NODE &&
  348. (forceContext === true || forceContext(node))
  349. ) {
  350. txt[++i] = innerText;
  351. txt[++i] = '';
  352. } else {
  353. if (typeof innerText[0] === 'string') {
  354. // Bridge nested text-node data so that they're
  355. // not considered their own contexts:
  356. // I.e. ['some', ['thing']] -> ['something']
  357. txt[i] += innerText.shift();
  358. }
  359. if (innerText.length) {
  360. txt[++i] = innerText;
  361. txt[++i] = '';
  362. }
  363. }
  364. } while (node = node.nextSibling);
  365.  
  366. return txt;
  367.  
  368. }
  369.  
  370. },
  371.  
  372. /**
  373. * Steps through the target node, looking for matches, and
  374. * calling replaceFn when a match is found.
  375. */
  376. processMatches: function() {
  377.  
  378. var matches = this.matches;
  379. var node = this.node;
  380. var elementFilter = this.options.filterElements;
  381.  
  382. var startPortion,
  383. endPortion,
  384. innerPortions = [],
  385. curNode = node,
  386. match = matches.shift(),
  387. atIndex = 0, // i.e. nodeAtIndex
  388. matchIndex = 0,
  389. portionIndex = 0,
  390. doAvoidNode,
  391. nodeStack = [node];
  392.  
  393. out: while (true) {
  394.  
  395. if (curNode.nodeType === Node.TEXT_NODE) {
  396.  
  397. if (!endPortion && curNode.length + atIndex >= match.endIndex) {
  398. // We've found the ending
  399. // (Note that, in the case of a single portion, it'll be an
  400. // endPortion, not a startPortion.)
  401. endPortion = {
  402. node: curNode,
  403. index: portionIndex++,
  404. text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex),
  405.  
  406. // If it's the first match (atIndex==0) we should just return 0
  407. indexInMatch: atIndex === 0 ? 0 : atIndex - match.startIndex,
  408.  
  409. indexInNode: match.startIndex - atIndex,
  410. endIndexInNode: match.endIndex - atIndex,
  411. isEnd: true
  412. };
  413.  
  414. } else if (startPortion) {
  415. // Intersecting node
  416. innerPortions.push({
  417. node: curNode,
  418. index: portionIndex++,
  419. text: curNode.data,
  420. indexInMatch: atIndex - match.startIndex,
  421. indexInNode: 0 // always zero for inner-portions
  422. });
  423. }
  424.  
  425. if (!startPortion && curNode.length + atIndex > match.startIndex) {
  426. // We've found the match start
  427. startPortion = {
  428. node: curNode,
  429. index: portionIndex++,
  430. indexInMatch: 0,
  431. indexInNode: match.startIndex - atIndex,
  432. endIndexInNode: match.endIndex - atIndex,
  433. text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex)
  434. };
  435. }
  436.  
  437. atIndex += curNode.data.length;
  438.  
  439. }
  440.  
  441. doAvoidNode = curNode.nodeType === Node.ELEMENT_NODE && elementFilter && !elementFilter(curNode);
  442.  
  443. if (startPortion && endPortion) {
  444.  
  445. curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion);
  446.  
  447. // processMatches has to return the node that replaced the endNode
  448. // and then we step back so we can continue from the end of the
  449. // match:
  450.  
  451. atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode);
  452.  
  453. startPortion = null;
  454. endPortion = null;
  455. innerPortions = [];
  456. match = matches.shift();
  457. portionIndex = 0;
  458. matchIndex++;
  459.  
  460. if (!match) {
  461. break; // no more matches
  462. }
  463.  
  464. } else if (
  465. !doAvoidNode &&
  466. (curNode.firstChild || curNode.nextSibling)
  467. ) {
  468. // Move down or forward:
  469. if (curNode.firstChild) {
  470. nodeStack.push(curNode);
  471. curNode = curNode.firstChild;
  472. } else {
  473. curNode = curNode.nextSibling;
  474. }
  475. continue;
  476. }
  477.  
  478. // Move forward or up:
  479. while (true) {
  480. if (curNode.nextSibling) {
  481. curNode = curNode.nextSibling;
  482. break;
  483. }
  484. curNode = nodeStack.pop();
  485. if (curNode === node) {
  486. break out;
  487. }
  488. }
  489.  
  490. }
  491.  
  492. },
  493.  
  494. /**
  495. * Reverts ... TODO
  496. */
  497. revert: function() {
  498. // Reversion occurs backwards so as to avoid nodes subsequently
  499. // replaced during the matching phase (a forward process):
  500. for (var l = this.reverts.length; l--;) {
  501. this.reverts[l]();
  502. }
  503. this.reverts = [];
  504. },
  505.  
  506. prepareReplacementString: function(string, portion, match) {
  507. var portionMode = this.options.portionMode;
  508. if (
  509. portionMode === PORTION_MODE_FIRST &&
  510. portion.indexInMatch > 0
  511. ) {
  512. return '';
  513. }
  514. string = string.replace(/\$(\d+|&|`|')/g, function($0, t) {
  515. var replacement;
  516. switch(t) {
  517. case '&':
  518. replacement = match[0];
  519. break;
  520. case '`':
  521. replacement = match.input.substring(0, match.startIndex);
  522. break;
  523. case '\'':
  524. replacement = match.input.substring(match.endIndex);
  525. break;
  526. default:
  527. replacement = match[+t] || '';
  528. }
  529. return replacement;
  530. });
  531.  
  532. if (portionMode === PORTION_MODE_FIRST) {
  533. return string;
  534. }
  535.  
  536. if (portion.isEnd) {
  537. return string.substring(portion.indexInMatch);
  538. }
  539.  
  540. return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length);
  541. },
  542.  
  543. getPortionReplacementNode: function(portion, match) {
  544.  
  545. var replacement = this.options.replace || '$&';
  546. var wrapper = this.options.wrap;
  547. var wrapperClass = this.options.wrapClass;
  548.  
  549. if (wrapper && wrapper.nodeType) {
  550. // Wrapper has been provided as a stencil-node for us to clone:
  551. var clone = doc.createElement('div');
  552. clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper);
  553. wrapper = clone.firstChild;
  554. }
  555.  
  556. if (typeof replacement == 'function') {
  557. replacement = replacement(portion, match);
  558. if (replacement && replacement.nodeType) {
  559. return replacement;
  560. }
  561. return doc.createTextNode(String(replacement));
  562. }
  563.  
  564. var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper;
  565.  
  566. if (el && wrapperClass) {
  567. el.className = wrapperClass;
  568. }
  569.  
  570. replacement = doc.createTextNode(
  571. this.prepareReplacementString(
  572. replacement, portion, match
  573. )
  574. );
  575.  
  576. if (!replacement.data) {
  577. return replacement;
  578. }
  579.  
  580. if (!el) {
  581. return replacement;
  582. }
  583.  
  584. el.appendChild(replacement);
  585.  
  586. return el;
  587. },
  588.  
  589. replaceMatch: function(match, startPortion, innerPortions, endPortion) {
  590.  
  591. var matchStartNode = startPortion.node;
  592. var matchEndNode = endPortion.node;
  593.  
  594. var precedingTextNode;
  595. var followingTextNode;
  596.  
  597. if (matchStartNode === matchEndNode) {
  598.  
  599. var node = matchStartNode;
  600.  
  601. if (startPortion.indexInNode > 0) {
  602. // Add `before` text node (before the match)
  603. precedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode));
  604. node.parentNode.insertBefore(precedingTextNode, node);
  605. }
  606.  
  607. // Create the replacement node:
  608. var newNode = this.getPortionReplacementNode(
  609. endPortion,
  610. match
  611. );
  612.  
  613. node.parentNode.insertBefore(newNode, node);
  614.  
  615. if (endPortion.endIndexInNode < node.length) { // ?????
  616. // Add `after` text node (after the match)
  617. followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode));
  618. node.parentNode.insertBefore(followingTextNode, node);
  619. }
  620.  
  621. node.parentNode.removeChild(node);
  622.  
  623. this.reverts.push(function() {
  624. if (precedingTextNode === newNode.previousSibling) {
  625. precedingTextNode.parentNode.removeChild(precedingTextNode);
  626. }
  627. if (followingTextNode === newNode.nextSibling) {
  628. followingTextNode.parentNode.removeChild(followingTextNode);
  629. }
  630. newNode.parentNode.replaceChild(node, newNode);
  631. });
  632.  
  633. return newNode;
  634.  
  635. } else {
  636. // Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order)
  637.  
  638.  
  639. precedingTextNode = doc.createTextNode(
  640. matchStartNode.data.substring(0, startPortion.indexInNode)
  641. );
  642.  
  643. followingTextNode = doc.createTextNode(
  644. matchEndNode.data.substring(endPortion.endIndexInNode)
  645. );
  646.  
  647. var firstNode = this.getPortionReplacementNode(
  648. startPortion,
  649. match
  650. );
  651.  
  652. var innerNodes = [];
  653.  
  654. for (var i = 0, l = innerPortions.length; i < l; ++i) {
  655. var portion = innerPortions[i];
  656. var innerNode = this.getPortionReplacementNode(
  657. portion,
  658. match
  659. );
  660. portion.node.parentNode.replaceChild(innerNode, portion.node);
  661. this.reverts.push((function(portion, innerNode) {
  662. return function() {
  663. innerNode.parentNode.replaceChild(portion.node, innerNode);
  664. };
  665. }(portion, innerNode)));
  666. innerNodes.push(innerNode);
  667. }
  668.  
  669. var lastNode = this.getPortionReplacementNode(
  670. endPortion,
  671. match
  672. );
  673.  
  674. matchStartNode.parentNode.insertBefore(precedingTextNode, matchStartNode);
  675. matchStartNode.parentNode.insertBefore(firstNode, matchStartNode);
  676. matchStartNode.parentNode.removeChild(matchStartNode);
  677.  
  678. matchEndNode.parentNode.insertBefore(lastNode, matchEndNode);
  679. matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode);
  680. matchEndNode.parentNode.removeChild(matchEndNode);
  681.  
  682. this.reverts.push(function() {
  683. precedingTextNode.parentNode.removeChild(precedingTextNode);
  684. firstNode.parentNode.replaceChild(matchStartNode, firstNode);
  685. followingTextNode.parentNode.removeChild(followingTextNode);
  686. lastNode.parentNode.replaceChild(matchEndNode, lastNode);
  687. });
  688.  
  689. return lastNode;
  690. }
  691. }
  692.  
  693. };
  694.  
  695. return exposed;
  696.  
  697. }));

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址