AO3 First Relationship Tag Filter

Hides works that don't have the relationship tag you're browsing as the first tag. Can be disabled when browsing other tags.

// ==UserScript==
// @name         AO3 First Relationship Tag Filter
// @namespace    https://greasyfork.org/en/users/1178894-sunny13
// @version      0.7.1
// @description  Hides works that don't have the relationship tag you're browsing as the first tag. Can be disabled when browsing other tags.
// @author       sunny13
// @include      http://archiveofourown.org/*
// @include      https://archiveofourown.org/*
// @grant        none
// @license      MIT
// @require     https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
// ==/UserScript==

(function($) {
    $(document).ready(function() {
        var tagsToSkip = ["Creator Chose Not To Use Archive Warnings", "Graphic Depictions Of Violence", "Major Character Death", "Rape/Non-Con", "Underage", "No Archive Warnings Apply"];
         console.log('Script is running');
        // Extract the relationship tag from the URL of the page you are browsing
        var currentURL = window.location.href;
        var relationshipTagFromURL = currentURL.match(/\/tags\/([^/]+)\/works(?:[?#]|$)|[?&]tag_id=([^&]*?)(?=&page=|&|$)/);

        if (!relationshipTagFromURL) {
            // If the relationship tag cannot be extracted, do nothing
            return;
        }

        function decodeAndNormalize(str) {
            // Decode URL-encoded characters
            const decodedStr = decodeURIComponent(str);

            // Normalize the string and remove diacritic marks
            const normalizedStr = decodedStr.normalize('NFD').replace(/[\u0300-\u036f]/g, '');

            return normalizedStr;
        }

        // Extract and convert the relationship tag to the desired format
        var rawRelationshipTag = relationshipTagFromURL[1] || relationshipTagFromURL[2];
        var extractedRelationshipTag = decodeAndNormalize(rawRelationshipTag.replace(/%20/g, ' ')
            .replace(/\*s\*/g, '/')
            .replace(/%7C/g, '|')
            .replace(/\*a\*/g, '&')
            .replace(/\+/g, ' ')
            .replace(/%28/g, '(')
            .replace(/%22/g, '"')
            .replace(/%29/g, ')')
            .replace(/\*d\*/g, '.'));
        var simplifiedRelationshipTag = extractedRelationshipTag.replace(/\s*\([^)]*\)\s*/g, '').replace(/\s*&\s*/g, ' & ').replace(/(\/\s*)you\b/gi, "$1reader").replace(/(&\s*)you\b/gi, "$1reader").replace(/Your\s+Name/g, "reader").replace(/\by\/n\b/gi, "reader").replace(/"/g, '').trim();
        var reversedRelationshipTag = extractedRelationshipTag.split('/').reverse().join('/');
        var reversedPlatonicTag = extractedRelationshipTag.replace(' & ', '&').split('&').reverse().join(' & ');
        var reversedSimplifiedRelationshipTag = simplifiedRelationshipTag.split('/').reverse().join('/');
        var reversedSimplifiedPlatonicTag = simplifiedRelationshipTag.replace(' & ', '&').split('&').reverse().join(' & ');
        var refinedRelationshipTag = simplifiedRelationshipTag.replace(/\s*\|\s*(?![^|&\/]*[&\/])[^|]*$/, '');
        var reversedRefinedRelationshipTag = refinedRelationshipTag.split('/').reverse().join('/');
        var reversedRefinedPlatonicTag = refinedRelationshipTag.replace(' & ', '&').split('&').reverse().join(' & ');

        // Function to generate alternative sides (AltSides)
function generateAltSides(side) {
    var words = side.split(' '); // Split side into words
    var altSides = [];

    // Generate all contiguous combinations
    for (var i = 0; i < words.length; i++) {
        for (var j = i; j < words.length; j++) {
            var altSide = words.slice(i, j + 1).join(' ');
            if (!altSides.includes(altSide)) {
                altSides.push(altSide);
            }
        }
    }

    // Generate non-contiguous combinations like "James Barnes"
    for (var i = 0; i < words.length; i++) {
        for (var j = i + 1; j < words.length; j++) {
            var altSide = words[i] + ' ' + words[j];
            if (!altSides.includes(altSide)) {
                altSides.push(altSide);
            }
        }
    }

    console.log('AltSides for side:', side, altSides);
    return altSides;
}

        // Function to generate tag variation
        function generateTagVariation(simplifiedRelationshipTag) {
            var sides = simplifiedRelationshipTag.split(/\s*\/\s*|\s*&\s*/); // Split into sides using "/" or "&"
            var altSidesList = sides.map(generateAltSides);

            // Generate all possible combinations of AltSides
            var tagVariations = altSidesList.reduce(function (combinations, altSides) {
                return combinations.flatMap(function (combination) {
                    return altSides.map(function (altSide) {
                        return combination.concat(altSide);
                    });
                });
            }, [[]]);

            // Join AltSides with original delimiters to create tagVariation
            var tagVariationList = tagVariations.map(function (variation) {
                return variation.join(simplifiedRelationshipTag.includes('/') ? '/' : ' & ');
            });

            console.log('Tag Variations:', tagVariationList);
            return tagVariationList;
        }

        // Function to generate reverse tag variation
        function generateReverseTagVariation(tagVariationList) {
            var reverseTagVariations = tagVariationList.flatMap(function (originalTag) {
                var newSides = originalTag.split(/\s*\/\s*|\s*&\s*/); // Split the original tag into NewSides
                var newSidesPermutations = permute(newSides); // Generate all possible permutations of NewSides

                // Join NewSides with original delimiters to create reverseTagVariation
                return newSidesPermutations.map(function (permutation) {
                    return permutation.join(originalTag.includes('/') ? '/' : ' & ');
                });
            });

            console.log('Reverse Tag Variations:', reverseTagVariations);
            return reverseTagVariations;
        }

        // Function to generate permutations of an array
        function permute(arr) {
            if (arr.length === 0) {
                return [[]];
            }

            var result = [];
            for (var i = 0; i < arr.length; i++) {
                var rest = [...arr.slice(0, i), ...arr.slice(i + 1)];
                var permutations = permute(rest);
                for (var j = 0; j < permutations.length; j++) {
                    result.push([arr[i], ...permutations[j]]);
                }
            }

            return result;
        }

        var tagVariations = generateTagVariation(simplifiedRelationshipTag);
        var reverseTagVariations = generateReverseTagVariation(tagVariations);

        $('<style>').text(
            '.workhide{border: 1px solid #ccc; margin: 0.643em 0em; padding: 0.429em 0.75em; height: 29px; display: block !important;}' +
            '.workhide .left{float: left; padding-top: 5px;}' +
            '.workhide .right{float: right;}'
        ).appendTo($('head'));

        // Function to check if the filter should be applied
        function shouldApplyFilter() {
            var filterEnabled = localStorage.getItem('filterEnabled');
            return filterEnabled !== 'false';
        }

        // Function to apply or remove the filter
        function applyOrRemoveFilter() {
            if (shouldApplyFilter()) {
                applyFilter();
            } else {
                removeFilter();
            }
        }

        // Function to hide works
        function applyFilter() {
            // Iterate through each work blurb on the page
            $('.index .blurb').each(function() {
                var tags = $(this).find('ul.tags');
                var tags2 = $(tags).find('.relationships a').toArray().map(tag => $(tag).text().trim());
                var relationshipTags = $(tags).find('.relationships a').toArray().map(tag => {
                    return decodeAndNormalize($(tag).text().trim().toLowerCase()
                        .replace(/\s*\|\s*(?![^|&\/]*[&\/])[^|]*$/, '')
                        .replace(/\s*-\s*relationship/, '')
                        .replace(/"/g, '')
                        .replace(/ x /g, '/')
                        .replace(/implied\s*/, '')
                        .replace(/\s*\([^)]*\)\s*/g, '')
                        .replace(/\s*&\s*/g, ' & ')
                        .replace(/(\/\s*)you\b/gi, "$1reader")
                        .replace(/(&\s*)you\b/gi, "$1reader")
                        .replace(/your\s+name/g, "reader")
                        .replace(/\by\/n\b/gi, "reader")
                        .replace(/female\s+reader/g, "reader")
                        .replace(/male\s+reader/g, "reader")
                        .replace(/fem!reader/g, "reader")
                        .replace(/xreader/g, "/reader").trim());
                });

                var shouldHide = false; // Flag to determine if we should hide the work blurb

                for (var i = 0; i < relationshipTags.length; i++) {
                    var tag = relationshipTags[i];

                    if (tagsToSkip.includes(tag)) {
                        shouldHide = true;
                    } else {
                        // Check if the first non-skipped tag matches the extracted relationship tag
                        if (shouldHide || tag !== extractedRelationshipTag.toLowerCase() && tag !== simplifiedRelationshipTag.toLowerCase() && tag !== reversedRelationshipTag.toLowerCase() && tag !== reversedSimplifiedRelationshipTag.toLowerCase() && tag !== reversedSimplifiedPlatonicTag.toLowerCase() && tag !== reversedPlatonicTag.toLowerCase() && tag !== refinedRelationshipTag.toLowerCase() && tag !== reversedRefinedRelationshipTag.toLowerCase() && tag !== reversedRefinedPlatonicTag.toLowerCase() && !tagVariations.some(variation => typeof variation === 'string' && variation.toLowerCase() === tag) && !reverseTagVariations.some(variation => typeof variation === 'string' && variation.toLowerCase() === tag)) {
                            // Hide the work blurb
                            $(this).hide();
                            var firstTag = tags2.find(t => !tagsToSkip.includes(t));

                            // Add a button to show the work blurb
                            var button = document.createElement('div');
                            button.setAttribute('class', 'workhide');
                            button.innerHTML = `<div class="left">This work does not prioritize the tag you are browsing. The first tag on this work is: <b>${firstTag}</b></div><div class="right"><button type="button" class="showwork">Show Work</button></div>`;
                            $(this).after(button);

                            // Adjust the width of the left message to accommodate different tag lengths
                            var leftMessage = $(button).find('.left');
                            var buttonWidth = $(button).width();
                            var leftMessageWidth = buttonWidth - $(button).find('.right').width() - 5;
                            $(leftMessage).width(leftMessageWidth);

                            $(this).css('display', 'none'); // Hide
                             setTimeout(() => {
                            $(this).css('display', 'none'); // Force reflow
                            }, 0);

                            setTimeout(function() {
                                var leftMessage = $(button).find('.left');
                                var buttonHeight = $(leftMessage).outerHeight(true) + 5; // Adjust 5 for padding and margin
                                $(button).height(buttonHeight);
                            }, 0);

                        } else {
                            // Show the work blurb
                            $(this).show();
                        }
                        break; // No need to check the rest of the tags
                    }
                }
            });
        }

        // Function to remove the filter
        function removeFilter() {
            // Remove the filter by showing all works and hiding any "Show Work" buttons
            $('.index .blurb').show();
            $('.workhide').remove();
        }

        // Toggle the filter when the "Toggle Filter" button is clicked
        function toggleFilter() {
            var filterEnabled = localStorage.getItem('filterEnabled');
            if (filterEnabled !== 'false') {
                localStorage.setItem('filterEnabled', 'false');
                $('#toggleIndicator').text('Off'); // Update the indicator text to "Off"
            } else {
                localStorage.setItem('filterEnabled', 'true');
                $('#toggleIndicator').text('On'); // Update the indicator text to "On"
            }
            applyOrRemoveFilter();
        }

        // Function to add the dropdown menu
        function addDropdownMenu() {
            // Create the menu button
            var menuButton = $('<li class="dropdown"></li>').html('<a>First Tag Filter</a>');

            // Create the dropdown menu
            var dropdownMenu = $('<ul class="menu dropdown-menu"></ul>');

            // Create the "Toggle Filter" button with the initial text based on the current filter state
            var filterEnabled = localStorage.getItem('filterEnabled');
            var toggleButtonText = filterEnabled === 'true' ? 'On' : 'Off';
            var toggleButton = $('<li></li>').html('<a>Toggle Filter: <span id="toggleIndicator">' + toggleButtonText + '</span></a>');
            toggleButton.click(toggleFilter);

            // Add the "Toggle Filter" button to the menu
            dropdownMenu.append(toggleButton);

            // Add the menu button to the header menu
            $('ul.primary.navigation.actions li.search').before(menuButton);

            // Add the dropdown menu to the menu button
            menuButton.append(dropdownMenu);
        }

        // Apply or remove the filter when the page loads
        applyOrRemoveFilter();

        // Add the dropdown menu
        addDropdownMenu();

        // Show the hidden work blurb when the "Show Work" button is clicked
        $(document).on('click', '.showwork', function() {
            var workBlurb = $(this).parents('.workhide').prev();
            $(workBlurb).show();
            $(this).parents('.workhide').remove();
        });

        console.log('Script running');
    });
})(window.jQuery);