- // ==UserScript==
- // @name AO3: Replace Y/N in works with your name
- // @description replaces Y/N and other placeholders in xReader fic with the name of your choice
- // @author escctrl
- // @namespace https://greasyfork.org/en/users/906106-escctrl
- // @version 2.0
- // @match https://archiveofourown.org/works/*
- // @license MIT
- // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js
- // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js
- // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js
- // @grant none
- // ==/UserScript==
-
- var cfg_lines = "", cfg_on = false;
-
- // the function to deal with all the configuration - using jQueryUI for dialogs
- (function($) {
- 'use strict';
-
- // retrieve localStorage on page load
- if (!localStorage) {
- console.log("The userscript \"AO3: Replace Y/N in works with your name\" terminated early because local storage cannot be accessed");
- return false;
- }
- else loadconfig();
-
- // if no other script has created it yet, write out a "Userscripts" option to the main navigation
- if ($('#scriptconfig').length == 0) {
- $('#header ul.primary.navigation li.dropdown').last()
- .after(`<li class="dropdown" id="scriptconfig">
- <a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a>
- <ul class="menu dropdown-menu"></ul>
- </li>`);
- }
- // then add this script's config option to navigation dropdown
- $('#scriptconfig .dropdown-menu').append(`<li><a href="javascript:void(0);" id="opencfg_replaceYN">Replace Y/N</a></li>`);
-
- // if the background is dark, use the dark UI theme to match
- let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "ui-darkness" : "base";
-
- // adding the jQuery stylesheet to style the dialog, and fixing the interference of AO3's styling
- $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/${dialogtheme}/jquery-ui.css">`)
- .append(`<style tyle="text/css">
- #cfgdialog_replaceYN legend {font-size: inherit; height: auto; width: auto; opacity: inherit;}
- #cfgdialog_replaceYN form {box-shadow: revert; cursor:auto;}
- #cfgdialog_replaceYN fieldset {background: revert; box-shadow: revert;}
- #cfgdialog_replaceYN input[type='text'] { position: relative; top: 1px; padding: .4em; width: 40%; min-width: 5em; }
- #cfgdialog_replaceYN input[type='text'], #cfgdialog_replaceYN button { margin: 0.2em 0; }
- #cfgdialog_replaceYN fieldset p { padding-top: 0; padding-left: 0; padding-right: 0; }
- </style>`);
-
- // create the rows of placeholder/replacement text from what was previously stored
- let linesHTML;
- if (cfg_lines.size == 0) {
- linesHTML = `
- <input type="text" name="t1[in]" value="(Y/N),Y/N,(F/N),F/N,(G/N),G/N" placeholder="placeholder in fic"> →
- <input type="text" name="t1[out]" value="Given Name" placeholder="replacement text">
- <br/>
- <input type="text" name="t2[in]" value="(Y/L/N),Y/L/N,(L/N),L/N" placeholder="placeholder in fic"> →
- <input type="text" name="t2[out]" value="Family Name" placeholder="replacement text">`;
- }
- else {
- // resetting the numbers of the t# so we don't count up into the hundreds if people remove/add lines
- let i = 1;
- linesHTML = [];
- cfg_lines.forEach((val, key) => {
- linesHTML.push(`
- <input type="text" name="t${i}[in]" value="${val.in}" placeholder="placeholder in fic"> →
- <input type="text" name="t${i}[out]" value="${val.out}" placeholder="replacement text">`);
- i++;
- });
- linesHTML = linesHTML.join(`<br/>`);
- }
-
- // the config dialog container
- let cfg = document.createElement('div');
- cfg.id = 'cfgdialog_replaceYN';
- $(cfg).html(`<p>Enter the placeholders used in the fic in the first textfield, and what should replace them in the second textfield.</p>
- <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>
- <p>Don't worry about uppercase/lowercase, the placeholders are treated as case-insensitive.</p>
- <form>
- <fieldset><legend>Placeholders and Replacements</legend>
- ${linesHTML}
- <button class="ui-button ui-widget ui-corner-all" id="addmore">+ Add more</button>
- </fieldset>
- <fieldset><legend>Toggle functionality on/off</legend>
- <label for="replaceYN_onoff">Replace text automatically</label><input type="checkbox" name="replaceYN_onoff" id="replaceYN_onoff" ${(cfg_on==="true") ? 'checked="checked"' : ""}>
- </fieldset>
- <p style="font-size: 80%; font-style: italic;">Saving changes will refresh the page to make this configuration take effect immediately.</p>
- <!-- Allow form submission with keyboard without duplicating the dialog button -->
- <input type="submit" tabindex="-1" style="display: none;">
- </form>`);
-
- // attach it to the DOM so that selections work
- $("body").append(cfg);
-
- // turn checkboxes and radiobuttons into pretty buttons
- $( "#cfgdialog_replaceYN input[type='checkbox']" ).checkboxradio();
-
- let dialogwidth = parseInt($("body").css("width")); // parseInt ignores letters (px)
- dialogwidth = dialogwidth > 400 ? 400 : dialogwidth * 0.9;
-
- // initialize the dialog (but don't open it)
- $( "#cfgdialog_replaceYN" ).dialog({
- appendTo: "#main",
- modal: true,
- title: 'Replace Y/N Configuration',
- draggable: true,
- resizable: false,
- autoOpen: false,
- width: dialogwidth,
- position: {my:"center", at: "center top"},
- buttons: {
- Reset: deleteconfig,
- Save: setconfig,
- Cancel: closedialog
- }
- });
-
- function closedialog() {
- $( "#cfgdialog_replaceYN" ).dialog( "close" );
- }
-
- // on click of the menu, open the configuration dialog
- $("#opencfg_replaceYN").on("click", function(e) {
- $( "#cfgdialog_replaceYN" ).dialog('open');
- });
-
- // event triggers if form is submitted with the <enter> key
- $( "#cfgdialog_replaceYN form" ).on("submit", (e)=>{
- e.preventDefault();
- setconfig();
- });
-
- // event triggers if addmore button is clicked
- $( "#cfgdialog_replaceYN #addmore" ).on("click", (e)=>{
- e.preventDefault();
- // grab the previous row's t# and increment by one
- let next = $( "#cfgdialog_replaceYN #addmore" ).prev().attr('name');
- next = parseInt(next.match(/\d+/)[0])+1;
- // add a new line of placeholder/replacement text fields
- $( "#cfgdialog_replaceYN #addmore" ).before(`<br/>
- <input type="text" name="t${next}[in]" value="" placeholder="placeholder in fic"> →
- <input type="text" name="t${next}[out]" value="" placeholder="replacement text">`);
- });
-
- // functions to deal with the localStorage
- function loadconfig() {
- cfg_lines = new Map(JSON.parse( localStorage.getItem('script-replaceYN') ));
- cfg_on = localStorage.getItem('script-replaceYN-on');
- }
- function setconfig() {
- // grab form fields for easier selection later (as an array for iterating later)
- let allfields = $( "#cfgdialog_replaceYN form input[type=text]" ).toArray();
-
- // now we turn this into a [t# => { in: "placeholders", out: "text" }, t# => {},...]
- // that allows reducing it to a single storage item without repetition
- // 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
- // inside of each t# we're happy with an Object bc we only need to access the in/out keys, not iterate over them
- var mappedfields = new Map();
- allfields.forEach((field) => {
- let row = field.name.match(/^t\d+/)[0];
- let key = field.name.match(/\[(in|out)\]/)[1];
- if (!mappedfields.has(row)) mappedfields.set(row, {}); // initializing the row
- // setting the in/out values in that row by ellipse-"unwrapping" the existing value and adding a new key:value to it
- // to not name the key "key" but use its variable value (in/out), it has to be put into []
- mappedfields.set(row, {...mappedfields.get(row), [key]: field.value});
- });
-
- // rows where either in or out field is empty get deleted
- mappedfields.forEach((val, key) => { if (val.in == "" || val.out == "") mappedfields.delete(key); });
-
- // serialize the result for storage
- localStorage.setItem('script-replaceYN', JSON.stringify(Array.from( mappedfields.entries() )));
-
- // get and store enabling/disabling the logic
- cfg_on = $( "#cfgdialog_replaceYN #replaceYN_onoff" ).prop('checked') ? "true" : "false"; // needs to be string
- localStorage.setItem('script-replaceYN-on', cfg_on);
-
- // close the dialog and F5 the page, since changes will only apply on refresh
- closedialog();
- location.reload();
- }
- function deleteconfig() {
- // empties all fields in the form
- $('#cfgdialog_replaceYN form [name]').val("");
-
- // delete the localStorage
- localStorage.removeItem('script-replaceYN');
- localStorage.removeItem('script-replaceYN-on');
-
- // close the dialog and F5 the page to apply the changes
- closedialog();
- location.reload();
- }
-
- })(jQuery);
-
- // function to turn the configuration into actionable regex
- function cfg2regex() {
- let replacelist = [];
- cfg_lines.forEach((val, key) => {
- // val.in has to be split by comma, trimmed, and escaped
- let inArr = val.in.split(",");
-
- // val.out can be taken literal
- // each of the in's + the out then make a pair of values in an array. [in, out]
- inArr.forEach( (v, i) => {
- replacelist.push(Array( v.trim().replace(/[/.*+?^${}()|[\]\\]/g, '\\$&'), val.out ));
- });
- });
- return replacelist;
- }
-
- // function to run the text replacement on Y/N and [Y/]L/N etc
- // 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
- function replaceYN() {
- // don't run a replace if no name has been configured or if user turned the thing off
- if (cfg_lines.size > 0 && cfg_on == "true") {
-
- // turn the configuration into actionable regex
- let replacelist = cfg2regex();
-
- // run the replacement on each paragraph of the work
- document.querySelectorAll('#main #chapters .userstuff > *').forEach((p) => {
- // in each paragraph, now replace all instances of our placeholders (token[0] = in, token[1] = out)
- replacelist.forEach((token) => {
- p.innerHTML = p.innerHTML.replace(new RegExp(token[0], "ig"), token[1]);
- });
- });
- }
- }
-
- // replace text only when page finished loading
- if (document.readyState === 'complete') replaceYN();
- else window.addEventListener('load', () => replaceYN());
-
- // helper function to determine whether a color (the background in use) is light or dark
- // https://awik.io/determine-color-bright-dark-using-javascript/
- function lightOrDark(color) {
-
- // Variables for red, green, blue values
- var r, g, b, hsp;
-
- // Check the format of the color, HEX or RGB?
- if (color.match(/^rgb/)) {
- // If RGB --> store the red, green, blue values in separate variables
- color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
- r = color[1];
- g = color[2];
- b = color[3];
- }
- else {
- // If hex --> Convert it to RGB: http://gist.github.com/983661
- color = +("0x" + color.slice(1).replace(color.length < 5 && /./g, '$&$&'));
- r = color >> 16;
- g = color >> 8 & 255;
- b = color & 255;
- }
-
- // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
- hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
-
- // Using the HSP value, determine whether the color is light or dark
- if (hsp>127.5) { return 'light'; }
- else { return 'dark'; }
- }