AO3: Replace Y/N in works with your name

replaces Y/N and other placeholders in xReader fic with the name of your choice

  1. // ==UserScript==
  2. // @name AO3: Replace Y/N in works with your name
  3. // @description replaces Y/N and other placeholders in xReader fic with the name of your choice
  4. // @author escctrl
  5. // @namespace https://greasyfork.org/en/users/906106-escctrl
  6. // @version 2.0
  7. // @match https://archiveofourown.org/works/*
  8. // @license MIT
  9. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js
  10. // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. var cfg_lines = "", cfg_on = false;
  16.  
  17. // the function to deal with all the configuration - using jQueryUI for dialogs
  18. (function($) {
  19. 'use strict';
  20.  
  21. // retrieve localStorage on page load
  22. if (!localStorage) {
  23. console.log("The userscript \"AO3: Replace Y/N in works with your name\" terminated early because local storage cannot be accessed");
  24. return false;
  25. }
  26. else loadconfig();
  27.  
  28. // if no other script has created it yet, write out a "Userscripts" option to the main navigation
  29. if ($('#scriptconfig').length == 0) {
  30. $('#header ul.primary.navigation li.dropdown').last()
  31. .after(`<li class="dropdown" id="scriptconfig">
  32. <a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a>
  33. <ul class="menu dropdown-menu"></ul>
  34. </li>`);
  35. }
  36. // then add this script's config option to navigation dropdown
  37. $('#scriptconfig .dropdown-menu').append(`<li><a href="javascript:void(0);" id="opencfg_replaceYN">Replace Y/N</a></li>`);
  38.  
  39. // if the background is dark, use the dark UI theme to match
  40. let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "ui-darkness" : "base";
  41.  
  42. // adding the jQuery stylesheet to style the dialog, and fixing the interference of AO3's styling
  43. $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/${dialogtheme}/jquery-ui.css">`)
  44. .append(`<style tyle="text/css">
  45. #cfgdialog_replaceYN legend {font-size: inherit; height: auto; width: auto; opacity: inherit;}
  46. #cfgdialog_replaceYN form {box-shadow: revert; cursor:auto;}
  47. #cfgdialog_replaceYN fieldset {background: revert; box-shadow: revert;}
  48. #cfgdialog_replaceYN input[type='text'] { position: relative; top: 1px; padding: .4em; width: 40%; min-width: 5em; }
  49. #cfgdialog_replaceYN input[type='text'], #cfgdialog_replaceYN button { margin: 0.2em 0; }
  50. #cfgdialog_replaceYN fieldset p { padding-top: 0; padding-left: 0; padding-right: 0; }
  51. </style>`);
  52.  
  53. // create the rows of placeholder/replacement text from what was previously stored
  54. let linesHTML;
  55. if (cfg_lines.size == 0) {
  56. linesHTML = `
  57. <input type="text" name="t1[in]" value="(Y/N),Y/N,(F/N),F/N,(G/N),G/N" placeholder="placeholder in fic"> &rarr;
  58. <input type="text" name="t1[out]" value="Given Name" placeholder="replacement text">
  59. <br/>
  60. <input type="text" name="t2[in]" value="(Y/L/N),Y/L/N,(L/N),L/N" placeholder="placeholder in fic"> &rarr;
  61. <input type="text" name="t2[out]" value="Family Name" placeholder="replacement text">`;
  62. }
  63. else {
  64. // resetting the numbers of the t# so we don't count up into the hundreds if people remove/add lines
  65. let i = 1;
  66. linesHTML = [];
  67. cfg_lines.forEach((val, key) => {
  68. linesHTML.push(`
  69. <input type="text" name="t${i}[in]" value="${val.in}" placeholder="placeholder in fic"> &rarr;
  70. <input type="text" name="t${i}[out]" value="${val.out}" placeholder="replacement text">`);
  71. i++;
  72. });
  73. linesHTML = linesHTML.join(`<br/>`);
  74. }
  75.  
  76. // the config dialog container
  77. let cfg = document.createElement('div');
  78. cfg.id = 'cfgdialog_replaceYN';
  79. $(cfg).html(`<p>Enter the placeholders used in the fic in the first textfield, and what should replace them in the second textfield.</p>
  80. <p>You can enter multiple placeholders (that should all be replaced by the same text) in one line and separate them with a comma.</p>
  81. <p>Don't worry about uppercase/lowercase, the placeholders are treated as case-insensitive.</p>
  82. <form>
  83. <fieldset><legend>Placeholders and Replacements</legend>
  84. ${linesHTML}
  85. <button class="ui-button ui-widget ui-corner-all" id="addmore">+ Add more</button>
  86. </fieldset>
  87. <fieldset><legend>Toggle functionality on/off</legend>
  88. <label for="replaceYN_onoff">Replace text automatically</label><input type="checkbox" name="replaceYN_onoff" id="replaceYN_onoff" ${(cfg_on==="true") ? 'checked="checked"' : ""}>
  89. </fieldset>
  90. <p style="font-size: 80%; font-style: italic;">Saving changes will refresh the page to make this configuration take effect immediately.</p>
  91. <!-- Allow form submission with keyboard without duplicating the dialog button -->
  92. <input type="submit" tabindex="-1" style="display: none;">
  93. </form>`);
  94.  
  95. // attach it to the DOM so that selections work
  96. $("body").append(cfg);
  97.  
  98. // turn checkboxes and radiobuttons into pretty buttons
  99. $( "#cfgdialog_replaceYN input[type='checkbox']" ).checkboxradio();
  100.  
  101. let dialogwidth = parseInt($("body").css("width")); // parseInt ignores letters (px)
  102. dialogwidth = dialogwidth > 400 ? 400 : dialogwidth * 0.9;
  103.  
  104. // initialize the dialog (but don't open it)
  105. $( "#cfgdialog_replaceYN" ).dialog({
  106. appendTo: "#main",
  107. modal: true,
  108. title: 'Replace Y/N Configuration',
  109. draggable: true,
  110. resizable: false,
  111. autoOpen: false,
  112. width: dialogwidth,
  113. position: {my:"center", at: "center top"},
  114. buttons: {
  115. Reset: deleteconfig,
  116. Save: setconfig,
  117. Cancel: closedialog
  118. }
  119. });
  120.  
  121. function closedialog() {
  122. $( "#cfgdialog_replaceYN" ).dialog( "close" );
  123. }
  124.  
  125. // on click of the menu, open the configuration dialog
  126. $("#opencfg_replaceYN").on("click", function(e) {
  127. $( "#cfgdialog_replaceYN" ).dialog('open');
  128. });
  129.  
  130. // event triggers if form is submitted with the <enter> key
  131. $( "#cfgdialog_replaceYN form" ).on("submit", (e)=>{
  132. e.preventDefault();
  133. setconfig();
  134. });
  135.  
  136. // event triggers if addmore button is clicked
  137. $( "#cfgdialog_replaceYN #addmore" ).on("click", (e)=>{
  138. e.preventDefault();
  139. // grab the previous row's t# and increment by one
  140. let next = $( "#cfgdialog_replaceYN #addmore" ).prev().attr('name');
  141. next = parseInt(next.match(/\d+/)[0])+1;
  142. // add a new line of placeholder/replacement text fields
  143. $( "#cfgdialog_replaceYN #addmore" ).before(`<br/>
  144. <input type="text" name="t${next}[in]" value="" placeholder="placeholder in fic"> &rarr;
  145. <input type="text" name="t${next}[out]" value="" placeholder="replacement text">`);
  146. });
  147.  
  148. // functions to deal with the localStorage
  149. function loadconfig() {
  150. cfg_lines = new Map(JSON.parse( localStorage.getItem('script-replaceYN') ));
  151. cfg_on = localStorage.getItem('script-replaceYN-on');
  152. }
  153. function setconfig() {
  154. // grab form fields for easier selection later (as an array for iterating later)
  155. let allfields = $( "#cfgdialog_replaceYN form input[type=text]" ).toArray();
  156.  
  157. // now we turn this into a [t# => { in: "placeholders", out: "text" }, t# => {},...]
  158. // that allows reducing it to a single storage item without repetition
  159. // list of t# needs to be an iterable object we can access by key, ie. a Map(), bc we don't know how many there will be
  160. // inside of each t# we're happy with an Object bc we only need to access the in/out keys, not iterate over them
  161. var mappedfields = new Map();
  162. allfields.forEach((field) => {
  163. let row = field.name.match(/^t\d+/)[0];
  164. let key = field.name.match(/\[(in|out)\]/)[1];
  165. if (!mappedfields.has(row)) mappedfields.set(row, {}); // initializing the row
  166. // setting the in/out values in that row by ellipse-"unwrapping" the existing value and adding a new key:value to it
  167. // to not name the key "key" but use its variable value (in/out), it has to be put into []
  168. mappedfields.set(row, {...mappedfields.get(row), [key]: field.value});
  169. });
  170.  
  171. // rows where either in or out field is empty get deleted
  172. mappedfields.forEach((val, key) => { if (val.in == "" || val.out == "") mappedfields.delete(key); });
  173.  
  174. // serialize the result for storage
  175. localStorage.setItem('script-replaceYN', JSON.stringify(Array.from( mappedfields.entries() )));
  176.  
  177. // get and store enabling/disabling the logic
  178. cfg_on = $( "#cfgdialog_replaceYN #replaceYN_onoff" ).prop('checked') ? "true" : "false"; // needs to be string
  179. localStorage.setItem('script-replaceYN-on', cfg_on);
  180.  
  181. // close the dialog and F5 the page, since changes will only apply on refresh
  182. closedialog();
  183. location.reload();
  184. }
  185. function deleteconfig() {
  186. // empties all fields in the form
  187. $('#cfgdialog_replaceYN form [name]').val("");
  188.  
  189. // delete the localStorage
  190. localStorage.removeItem('script-replaceYN');
  191. localStorage.removeItem('script-replaceYN-on');
  192.  
  193. // close the dialog and F5 the page to apply the changes
  194. closedialog();
  195. location.reload();
  196. }
  197.  
  198. })(jQuery);
  199.  
  200. // function to turn the configuration into actionable regex
  201. function cfg2regex() {
  202. let replacelist = [];
  203. cfg_lines.forEach((val, key) => {
  204. // val.in has to be split by comma, trimmed, and escaped
  205. let inArr = val.in.split(",");
  206.  
  207. // val.out can be taken literal
  208. // each of the in's + the out then make a pair of values in an array. [in, out]
  209. inArr.forEach( (v, i) => {
  210. replacelist.push(Array( v.trim().replace(/[/.*+?^${}()|[\]\\]/g, '\\$&'), val.out ));
  211. });
  212. });
  213. return replacelist;
  214. }
  215.  
  216. // function to run the text replacement on Y/N and [Y/]L/N etc
  217. // sadly this can run only on initial page load - after that the work text has been changed and we wouldn't find the placeholders to replace
  218. function replaceYN() {
  219. // don't run a replace if no name has been configured or if user turned the thing off
  220. if (cfg_lines.size > 0 && cfg_on == "true") {
  221.  
  222. // turn the configuration into actionable regex
  223. let replacelist = cfg2regex();
  224.  
  225. // run the replacement on each paragraph of the work
  226. document.querySelectorAll('#main #chapters .userstuff > *').forEach((p) => {
  227. // in each paragraph, now replace all instances of our placeholders (token[0] = in, token[1] = out)
  228. replacelist.forEach((token) => {
  229. p.innerHTML = p.innerHTML.replace(new RegExp(token[0], "ig"), token[1]);
  230. });
  231. });
  232. }
  233. }
  234.  
  235. // replace text only when page finished loading
  236. if (document.readyState === 'complete') replaceYN();
  237. else window.addEventListener('load', () => replaceYN());
  238.  
  239. // helper function to determine whether a color (the background in use) is light or dark
  240. // https://awik.io/determine-color-bright-dark-using-javascript/
  241. function lightOrDark(color) {
  242.  
  243. // Variables for red, green, blue values
  244. var r, g, b, hsp;
  245.  
  246. // Check the format of the color, HEX or RGB?
  247. if (color.match(/^rgb/)) {
  248. // If RGB --> store the red, green, blue values in separate variables
  249. color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
  250. r = color[1];
  251. g = color[2];
  252. b = color[3];
  253. }
  254. else {
  255. // If hex --> Convert it to RGB: http://gist.github.com/983661
  256. color = +("0x" + color.slice(1).replace(color.length < 5 && /./g, '$&$&'));
  257. r = color >> 16;
  258. g = color >> 8 & 255;
  259. b = color & 255;
  260. }
  261.  
  262. // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
  263. hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
  264.  
  265. // Using the HSP value, determine whether the color is light or dark
  266. if (hsp>127.5) { return 'light'; }
  267. else { return 'dark'; }
  268. }