[AO3] Actually Relevant Characters

Sorts and highlights works based on how relevant the tagged character seems in the work's tags and summary.

  1. // ==UserScript==
  2. // @name [AO3] Actually Relevant Characters
  3. // @namespace https://greasyfork.org/en/users/1138163-dreambones
  4. // @version 1.2.2
  5. // @description Sorts and highlights works based on how relevant the tagged character seems in the work's tags and summary.
  6. // @author DREAMBONES
  7. // @match http*://archiveofourown.org/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=archiveofourown.org
  9. // @grant none
  10. // @license Can modify w/credit.
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. var disqualifiers = /(mention|implied)/i
  17. var boostedRelevance = /(centric|POV)/i
  18. var bumpedListPos = {
  19. "High": 0,
  20. "Medium": 0,
  21. "Low": 0
  22. }
  23.  
  24. var isCharacter = false;
  25. var charInfo = {
  26. "Name": null,
  27. "Alias": null,
  28. }
  29.  
  30. var domainRe = /https?:\/\/archiveofourown\.org\/(works|tags)(?!\/\d+)/i;
  31. if (domainRe.test(document.URL)) {
  32. var character = document.querySelector("h2.heading > a");
  33. var mainRe = character.innerHTML.match(/(?<name>[^()]+[^ (]) ?(?<media>\(.+\))?/);
  34. var names = mainRe.groups.name.split(" | ");
  35. charInfo.Name = names[0];
  36. if (names.length > 1) { charInfo.Alias = names[1]; }
  37.  
  38. var worksList = document.querySelectorAll("ol.work.index.group, ul.index.group, #user-series > ol.index.group");
  39. for (let section of worksList) {
  40. for (let work of section.children) {
  41. var bumped = false;
  42. var bumps = 0;
  43.  
  44. var charTags = work.querySelectorAll("ul.tags.commas > li.characters > a");
  45. for (let tag of charTags) { if (tag.innerHTML == character.innerHTML) { isCharacter = true; } }
  46. if (isCharacter) {
  47. var tagCt = 0;
  48. var charTagCt = 0;
  49. var ttlTagCt = 0;
  50.  
  51. var tags = work.querySelectorAll("ul.tags.commas > li > a");
  52. for (let tag of tags) {
  53. ttlTagCt++;
  54. if (tag.parentNode.className == "characters") { charTagCt++; }
  55. CheckTag(tag, work, section);
  56. }
  57.  
  58. try {
  59. var summary = work.querySelector("blockquote.userstuff.summary > p");
  60. CheckSummary();
  61. }
  62. catch (TypeError) { null; }
  63. }
  64. // console.log(`Character Tags: ${tagCt} of ${ttlTagCt} (${tagCt / ttlTagCt})`);
  65. if (charTagCt < 4) { bumps += 2; }
  66. else if ((tagCt / ttlTagCt) >= 0.15) { bumps += 1; }
  67. if (bumps > 0) { sendToTop(work, section); }
  68. }
  69. }
  70. }
  71.  
  72. function escapeRegExp(text) {
  73. return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
  74. }
  75.  
  76. function replaceAt(text, index, original, replacement) {
  77. return text.substring(0, index) + replacement + text.substring(index + original.length); // index + replacement.length
  78. }
  79.  
  80. function CheckTag(tag, work, section) {
  81. let type = tag.parentNode.className;
  82. let text = tag.innerHTML;
  83.  
  84. let alreadyMatched = false;
  85. for (let name in charInfo) {
  86. if (charInfo[name]) {
  87. let re = new RegExp(`${escapeRegExp(charInfo[name])}(?:'s)?(?!<\/span>)`, "gi");
  88. if (re.test(text) && !alreadyMatched) {
  89. alreadyMatched = true;
  90. if (disqualifiers.test(text) && type == "characters") { sendToBottom(work, section); }
  91. else {
  92. if (charTagCt == 1 && type == "characters") { bumps += 3; }
  93. if (type == "characters" && text == [charTags.length-1].innerHTML) { sendToBottom(work, section); }
  94. if (boostedRelevance.test(text)) { bumps += 3; }
  95. if (type == "relationships") { bumps += 1; }
  96. if (type == "freeforms") { bumps += 0.5; }
  97. tagCt++;
  98. }
  99. tag.style["text-transform"] = "uppercase";
  100. tag.style.border = `1px solid ${tag.style.color}`;
  101. }
  102. else if (disqualifiers.test(text) && !alreadyMatched) {
  103. if (type == "characters" || type == "relationships") { tag.style.opacity = 0.5; }
  104. alreadyMatched = true;
  105. }
  106. }
  107. }
  108. }
  109.  
  110. function CheckSummary() {
  111. let anyMatches = false;
  112. for (let name in charInfo) {
  113. if (charInfo[name]) {
  114. let re = new RegExp(`${escapeRegExp(charInfo[name])}(?:'s)?(?!<\/span>)`, "gi");
  115. let matches = summary.innerHTML.match(re);
  116. if (matches) {
  117. anyMatches = true;
  118. for (let match of matches) {
  119. let index = summary.innerHTML.search(re);
  120. summary.innerHTML = replaceAt(summary.innerHTML, index, match, `<span style="text-transform: uppercase; text-decoration: underline">${match}</span>`);
  121. }
  122. }
  123. }
  124. }
  125. if (anyMatches) { bumps += 2; }
  126. }
  127.  
  128. function sendToTop(work, section) {
  129. if (!bumped && work.style.opacity != 0.5) {
  130. bumped = true;
  131. var color;
  132. try { color = summary.style.color; }
  133. catch (TypeError) { color = work.querySelector("dl.stats").style.color; }
  134. if (bumps == 1) {
  135. work.style.border = `3px dashed ${color}`;
  136. section.insertBefore(work, section.children[bumpedListPos.Low]);
  137. bumpedListPos.Low++;
  138. }
  139. else if (bumps >= 2 && bumps < 3) {
  140. work.style.border = `3px solid ${color}`;
  141. section.insertBefore(work, section.children[bumpedListPos.Medium]);
  142. bumpedListPos.Medium++;
  143. bumpedListPos.Low++;
  144. }
  145. else if (bumps >= 3) {
  146. work.style.border = `5px double ${color}`;
  147. section.insertBefore(work, section.children[bumpedListPos.High]);
  148. bumpedListPos.High++;
  149. bumpedListPos.Medium++;
  150. bumpedListPos.Low++;
  151. }
  152. }
  153. }
  154.  
  155. function sendToBottom(work, section) {
  156. work.style.opacity = "0.5";
  157. section.append(work);
  158. }
  159. })();