Ugly journalists

Caught You.

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

QingJ © 2025

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