AO3: [Wrangling] Search Term Highlighting

highlights the search terms in the results

当前为 2024-07-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AO3: [Wrangling] Search Term Highlighting
  3. // @namespace https://greasyfork.org/en/users/906106-escctrl
  4. // @description highlights the search terms in the results
  5. // @author escctrl
  6. // @version 6.0
  7. // @match *://*.archiveofourown.org/tags/search?*
  8. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js
  9. // @require https://update.greasyfork.org/scripts/491888/1355841/Light%20or%20Dark.js
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. /* eslint-disable no-multi-spaces */
  14. /* global jQuery, lightOrDark */
  15.  
  16. // CONFIGURATION
  17.  
  18. // this impacts how the highlighting acts with special characters (when there aren't asterisk in the search term -- yes, AO3 is being extra)
  19. // false = like AO3 searches by ignoring special characters
  20. // true = as you probably meant it, by searching for the literal special character
  21. const STRICT_HIGHLIGHT = true;
  22.  
  23.  
  24. (function($) {
  25. 'use strict';
  26.  
  27. const DEBUG = false;
  28.  
  29. const HIGHLIGHT = lightOrDark(window.getComputedStyle(document.body).backgroundColor) == "dark" ? '#082f03' : '#FFFF99' ;
  30.  
  31. // and some more CSS styling for the table
  32. document.getElementsByTagName('head')[0].innerHTML += `<style type="text/css">
  33. #highlightButton { font-size: 0.6em; margin-right: 0.5em; }
  34. #highlightButton.pressed { color: #900; border-top: 1px solid #999; border-left: 1px solid #999; box-shadow: inset 2px 2px 2px #bbb; }
  35. a.tag span.highlight.on { background-color: ${HIGHLIGHT}; }
  36. a.tag:hover span.highlight.on { background-color: inherit; }
  37. </style>`;
  38.  
  39. // *** Highlight the search terms in the results
  40.  
  41. // a button to toggle the highlighting on and off
  42. var button = document.createElement("button");
  43. button.innerHTML = "Highlight Search Terms";
  44. button.id = "highlightButton";
  45. button.addEventListener("click", HighlightTerms);
  46. $("#main > h3").prepend(button);
  47.  
  48. // first we grab the search term from the input field (which sanitizes HTML characters for us)
  49. var search_name = document.getElementById('tag_search_name').value;
  50.  
  51. // deal with quoted terms that may include whitespaces
  52. var quotedterms = search_name.match(/".*?"/gi); // find all quoted searches (before we start splitting the string)
  53. search_name = search_name.replace(/".*?"/gi, ""); // remove the matches from the original search terms
  54.  
  55. search_name = search_name.split(" "); // now we can split the remaining search terms on spaces
  56. // and join the two arrays back together for further processing, but remove any null values (happen if either one of the original arrays is empty)
  57. search_name = [].concat(search_name, quotedterms).filter((val) => val !== null);
  58.  
  59. // this isn't the magic bullet, but try sorting by length (from longest to shortest search term) to get around problems with highlighting matches within longer matches
  60. // hopefully it manages to avoid most situations where a longer match would not be recognized because a shorter match already added placeholders in the middle of the term
  61. // we're comparing lengths without quotes and asterisk, since they probably won't matter later
  62. search_name = search_name.sort((a,b) => b.replace(/["*]/gi, "").length - a.replace(/["*]/gi, "").length);
  63.  
  64. // turn the user input into regular expressions
  65. var search_arr = search_name.map( Terms2Regex );
  66. var search_arr2 = [];
  67.  
  68. // at this point, some items in the search_arr might be an array of even more search terms... need to flatten
  69. search_arr.forEach( function(e, i) {
  70. if (Array.isArray(e)) search_arr2 = search_arr2.concat(e);
  71. else search_arr2.push(e);
  72. });
  73.  
  74. // running through the tag names, and highlighting the matched parts by adding a <span> with a class
  75. $('a.tag').each( function(i, link) {
  76. var tagname = link.text;
  77.  
  78. if (DEBUG) console.log(tagname);
  79.  
  80. search_arr2.forEach( function(term) {
  81. // we add only a placeholder, so we won't start matching against the HTML characters
  82. tagname = tagname.replace(new RegExp(term, "gi"), "\r$&\t");
  83. });
  84. // once done with finding term matches, we can turn the placeholders into actual HTML and display that
  85. link.innerHTML = tagname.replace(/\r/g, "<span class='highlight'>").replace(/\t/g, "</span>");
  86. if (DEBUG) console.log(link.innerHTML);
  87. });
  88.  
  89. function HighlightTerms() {
  90. $('a.tag span.highlight').toggleClass("on");
  91. $(button).toggleClass("pressed");
  92. }
  93.  
  94. function Terms2Regex(term) {
  95.  
  96. // skip bad input and logical operators
  97. switch (term) {
  98. case "": case null:
  99. case "OR": case "NOT": case "AND":
  100. case "||": case "&&":
  101. return false;
  102. }
  103.  
  104. // we clone our regex into a new variable, and keep the original search term unchanged for comparisons
  105. // get rid of asterisk at beginning and end, as well as any quotes
  106. var regex = term.replace(/(^\*)|(\*$)|(")/g, '');
  107. var regex_split = [];
  108.  
  109. // special characters are essentially ignored, if no asterisk is present in the search term and as long as there's a word character
  110. // if user wants to follow AO3's search logic, we need to modify the regex accordingly
  111. if (term.indexOf('*') == -1 && regex.match(/\w/gi) !== null && !STRICT_HIGHLIGHT) {
  112.  
  113. // at the borders of a word, the special character simply disappears
  114. regex = regex.replace(/(^\s*[^\w\s]+\s*)|(\s*[^\w\s]+\s*$)/g, '');
  115.  
  116. // when surrounded by \w's (we already removed the other characters at beginning/end)...
  117. // with quotes, it acts like a non-word wildcard between them
  118. if (term.slice(0,1) == '"' && term.slice(-1) == '"') regex = regex.replace(/\s*[^\w\s]+\s*/g, "\[^\\w\]*");
  119. // without quotes, it splits the term in two
  120. else regex_split = regex.replace(/\s*[^\w\s]+\s*/g, ",").split(',');
  121. }
  122. // some characters have special meaning in regex, we need to add an escape sequence to highlight them
  123. // except asterisk! (at first) -- then replace any remaining asterisk in the middle with the regex equivalent
  124. else regex = regex.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*?');
  125.  
  126. // finish creating the regexes by adding the boundaries to (each part of) the term
  127. if (regex_split.length > 0) {
  128. regex_split.forEach( function(v, i) { regex_split[i] = addBoundaries(v, term); });
  129. regex = regex_split;
  130. }
  131. else regex = addBoundaries(regex, term);
  132.  
  133. if (DEBUG) console.log(term + ' => ' + regex);
  134.  
  135. // this will return an array of strings, in case the term had to be split. otherwise a single string
  136. return regex;
  137. }
  138.  
  139. // add the regex Metacharacter for word boundaries, only if the first/last letter was a 'word' character
  140. function addBoundaries(rgx, term) {
  141. if (term.slice(0,1) != "*" && rgx.match(/^\w/gi) !== null) { rgx = "\\b" + rgx; }
  142. if (term.slice(-1) != "*" && rgx.match(/\w$/gi) !== null) { rgx = rgx + "\\b"; }
  143.  
  144. return rgx;
  145. }
  146.  
  147. })(jQuery);