Greasy Fork is available in English.

AO3: Replace Y/N in works with your name

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

// ==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"> &rarr;
        <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"> &rarr;
        <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"> &rarr;
            <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"> &rarr;
        <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'; }
}