Greasy Fork

来自缓存

Greasy Fork is available in English.

AO3: Prioritise My Faves

Hide work if chosen tags are late in sequence, or if blacklisted tags are early

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AO3: Prioritise My Faves
// @namespace    http://greasyfork.icu/en/users/757649-certifieddiplodocus
// @version      2.0.0
// @description  Hide work if chosen tags are late in sequence, or if blacklisted tags are early
// @author       CertifiedDiplodocus
// @match        http*://archiveofourown.org/works*
// @match        http*://archiveofourown.org/tags*
// @exclude      /\/works\/[0-9].*/
// @exclude      /^https?:\/\/archiveofourown\.org(?!.*\/works)/
// @icon         https://raw.githubusercontent.com/EmeraldBoa/Userscripts-by-a-Certified-Diplodocus/refs/heads/main/images/icons/ao3-logo-by-bingeling-GPL.svg
// @license      GPL-3.0-or-later
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValues
// @grant        GM_deleteValues
// ==/UserScript==

/* global GM_getValues, GM_deleteValues */

/* AO3 logo designed by bingeling. Licensed under GNU 2+ https://commons.wikimedia.org/wiki/File:Archive_of_Our_Own_logo.png

Currently active on works/* and tags/* pages. To also enable on user pages, add the following line in the header:
    // @match        http*://archiveofourown.org/users*

'------------------------------------------------------------------------------------------------------------------ */
(function () {
    'use strict'
    const $ = document.querySelector.bind(document) // shorthand for readability
    const $$ = document.querySelectorAll.bind(document)

    // get settings from script storage (default values if not)
    const stored = GM_getValues({
        menuIsExpanded: false,
        filterIsOn: true,
        characters: null,
        relationships: null,
        excludedCharacters: null,
        excludedRelationships: null,
        format: 'wildcard',
        ao3SaviorIsInstalled: true,
        debugMode: false,
    })
    let noFilterYet = true
    let debugModeOn = stored.debugMode

    // get AO3 elements; validate
    const works = $$('.work.blurb')
    const workslistContainer = $('ol.work')
    const ao3FilterSidebar = $('#work-filters')

    const sidebarHTML = `<h3 class="landmark heading">Tag priority</h3>    
    <fieldset class="pmf__ui pmf__sidebar">
        <legend>Tag priority:</legend> <!--is this redundant?-->
        <dl>
            <dt class="pmf__sidebar-head collapsed">
                <button type="button" class="expander" aria-expanded="false" aria-controls="pmf__sidebar-content">
                    Tag priority
                </button>
                <button type="button" id="pmf__filter-toggle" class="current" aria-pressed="true">On</button>
            </dt>
            <dd id="pmf__sidebar-content" class="expandable">
                <section class="pmf__wrap" aria-describedby="pmf__header-include">
                    <div class="pmf__head">
                        <h4 id="pmf__header-include">Include</h4> (at least one)
                        <button type="button" class="question" aria-label="help">?</button>
                    </div>
                    <section class="pmf__tag-block include characters">
                        <label id="pmf__include-chars">
                            <input type="checkbox">
                            <span class="indicator" aria-hidden="true"></span>
                            <span id="block-include-chars"><span class="landmark">Include </span>Characters</span>
                        </label>
                        <textarea class="pmf__tag-list" rows="3" autocomplete="off" autocapitalize="off"
                            spellcheck="false" placeholder="Gilgamesh, Enkidu" readonly></textarea>
                        <label class="pmf__within">...within the first <input type="text" class="pmf__tag-lim" readonly>
                            tags</label>
                    </section>
                    <section class="pmf__tag-block include relationships">
                        <label>
                            <input type="checkbox">
                            <span class="indicator" aria-hidden="true"></span>
                            <span><span class="landmark">Include </span>Relationships</span>
                        </label>
                        <textarea class="pmf__tag-list" rows="3" autocomplete="off" autocapitalize="off"
                            spellcheck="false" placeholder="Gilgamesh*Enkidu, Enkidu*Gilgamesh" readonly></textarea>
                        <label class="pmf__within">...within the first <input type="text" class="pmf__tag-lim" readonly>
                            tags</label>
                    </section>
                </section>
                <section class="pmf__wrap" aria-describedby="pmf__header-exclude">
                    <div class="pmf__head">
                        <h4 id="pmf__header-exclude">Exclude</h4>
                        <button type="button" class="question" aria-label="help">?</button>
                    </div>
                    <section class="pmf__tag-block exclude characters">
                        <label>
                            <input type="checkbox">
                            <span class="indicator" aria-hidden="true"></span>
                            <span><span class="landmark">Exclude </span>Characters</span>
                        </label>
                        <textarea class="pmf__tag-list" rows="3" autocomplete="off" autocapitalize="off"
                            spellcheck="false" readonly></textarea>
                        <label class="pmf__within">...within the first <input type="text" class="pmf__tag-lim" readonly>
                            tags</label>
                    </section>
                    <section class="pmf__tag-block exclude relationships">
                        <label>
                            <input type="checkbox">
                            <span class="indicator" aria-hidden="true"></span>
                            <span><span class="landmark">Exclude </span>Relationships</span>
                        </label>
                        <textarea class="pmf__tag-list" rows="3" autocomplete="off" autocapitalize="off"
                            spellcheck="false" readonly></textarea>
                        <label class="pmf__within">...within the first <input type="text" class="pmf__tag-lim" readonly>
                            tags</label>
                    </section>
                </section>
                <section class="pmf__syntax" aria-describedby="pmf__syntax-header">
                    <h3 id="pmf__syntax-header" class="landmark">Format</h3>
                    <fieldset>
                        <legend>Format</legend>
                        <label>
                            <input type="radio" name="format" id="pmf__opt-wildcard" value="wildcard" form=""
                                checked><!--omit from parent form-->
                            <span class="indicator" aria-hidden="true"></span>
                            <span>wildcards (*)</span>
                        </label>
                        <label>
                            <input type="radio" name="format" id="pmf__opt-regex" value="regex"
                                form=""><!--omit from parent form-->
                            <span class="indicator" aria-hidden="true"></span>
                            <span>regex</span>
                        </label>
                        <button type="button" class="question" aria-label="help">?</button>
                    </fieldset>
                </section>
                <section class="actions" aria-describedby="pmf__header-submit">
                    <h3 id="pmf__header-submit" class="landmark">
                        Submit
                    </h3><button class="pmf__apply" type="button">Apply filters</button>
                </section>
                <dl>
                    <dt class="pmf__config-head collapsed">
                        <button type="button" class="expander" aria-expanded="false" aria-controls="pmf__config"
                            aria-label="Settings">⚙️ Settings</button>
                        <p class="footnote">
                            <a href="#work-filters">Clear Filters</a>
                        </p>
                    </dt>
                    <dd id="pmf__config" class="expandable">
                        <div></div>
                        <label>
                            <input id="pmf__setting-AO3-sav" type="checkbox">
                            <span class="indicator" aria-hidden="true"></span>
                            <span>AO3 Savior is installed</span>
                            <span class="pmf__explanatory-text">(prevent conflict on load)</span>
                        </label>
                        <label>
                            <input id="pmf__setting-debug" type="checkbox">
                            <span class="indicator" aria-hidden="true"></span>
                            <span>Debug mode</span>
                            <span class="pmf__explanatory-text">(may require a reload)</span>
                        </label>
                    </dd>
                </dl>
            </dd>
        </dl>
    </fieldset>`
    const infoModalHTML = `<div class="pmf__ui popup">
        <details class="pmf__info-section">
            <summary>Basics</summary>
            <p>The script cannot use AO3's tag system, but instead <strong>matches
                    the literal tag text</strong>. <br>
                Add synonyms where necessary
                (e.g. <span class="search-term">Naruto Uzumaki, Uzumaki Naruto</span>).
                When searching for a specific relationship, you MUST include both permutations:
                <span class="search-term">A/B</span> and <span class="search-term">B/A</span>.
            </p>
            <ul>
                <li>Only character and relationship tags are checked.</li>
                <li>Comma = OR
                    <table class="pmf__matching-basics">
                        <thead>
                            <tr>
                                <th scope="col"></th>
                                <th scope="col">search term(s)</th>
                                <th scope="col">N</th>
                                <th scope="col">result</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td>include characters</td>
                                <td>enkidu, gilgamesh</td>
                                <td>4</td>
                                <td>SHOW fics with "Enkidu" OR "Gilgamesh" within the first 4 character tags</td>
                            </tr>
                            <tr>
                                <td>exclude characters</td>
                                <td>enkidu, gilgamesh</td>
                                <td>4</td>
                                <td>HIDE fics with "Enkidu" OR "Gilgamesh" within the first 4 character tags</td>
                            </tr>
                        </tbody>
                    </table>
                </li>
                <li>
                    Search is case-insensitive and ignores diacritics (a = á, ä, ā), line breaks and extra spaces.
                    (Characters like ñ, ł, æ, ø, are letters, not diacritics: searching <span
                        class="search-term">Inigo</span> will match "<span class="str-match">Ín</span>igo" but not
                    "<span class="str-match">Íñ</span>igo")
                </li>
            </ul>
            <p>⚠ If you need a permanent blacklist, use <a href="http://greasyfork.icu/en/scripts/3579-ao3-savior">AO3 savior</a>.</p>
        </details>
        <details class="pmf__info-section">
            <summary>How to filter</summary>
            <p>Choose between <strong>*wildcards</strong> (default) or <strong>regex</strong> (more flexible, for
                advanced users). <br>
                Wildcards are enough for most purposes:</p>
            <ul>
                <li>
                    <span class="search-term">uzumaki</span> ➜ matches "Naruto <span class="str-match">Uzumaki</span>",
                    "<span class="str-match">Uzumaki</span> Kushina"
                    but NOT "Uzumaki<span class="str-match">s</span>"
                </li>
                <li>
                    <span class="search-term">shika*</span> ➜ matches "<span class="str-match">Shika</span>ku" and
                    "<span class="str-match">Shika</span>maru Nara" but NOT "<span class="str-match">I</span>shikawa"
                </li>
            </ul>
            <p>Regex grants finer control, for example...</p>
            <ul>
                <li>alternate spellings: <span class="search-term">[RL]i[zs]a</span> ➜ Riza/Risa/Liza/Lisa
                </li>
                <li>limiting options: <span class="search-term">(Arnold|Ace).*Rimmer</span> ➜ don't match John or
                    Harold
                    Rimmer</li>
                <li>exclusions: <span class="search-term">[^(]doctor</span> ➜ match "The&nbsp;Doctor" but NOT
                    "The&nbsp;Master&nbsp;(Doctor&nbsp;Who)"
                </li>
            </ul>
            <details class="pmf__rx-cheatsheet">
                <summary>Regex cheatsheet</summary>
                <h4>Characters</h4>
                <dl>
                    <dt>.</dt>
                    <dd>any character</dd>
                    <dt>[abc]</dt>
                    <dd>any of a, b, or c</dd>
                    <dt>[^abc]</dt>
                    <dd>not a, b, or c</dd>
                    <dt>[a-g]</dt>
                    <dd>character between a &amp; g</dd>
                </dl>
                <h4>Operators</h4>
                <dl>
                    <dt>a* a+ a?</dt>
                    <dd>0 or more, 1 or more, 0 or 1 (.* = 0 or more of any character)</dd>
                    <dt>^abc$</dt>
                    <dd>start / end of the string (matches the exact tag "abc")</dd>
                    <dt>ab|cd</dt>
                    <dd>match ab OR cd</dd>
                    <dt>(abc)</dt>
                    <dd>group - e.g. (ab|cd)xy matches abxy or cdxy</dd>
                </dl>
                <h4>Special characters</h4>
                <dl>
                    <dt>. + * ? <br>^ $ | &#92; <br>{ } ( ) [ ]</dt>
                    <dd>
                        <p>can be:</p>
                        <ul>
                            <li>replaced with "." (=match any character)</li>
                            <li>escaped with &#92; ➜ &#92;? </li>
                            <li>enclosed in [] ➜ [?]</li>
                        </ul>
                    </dd>
                </dl>
                <p>See <a href="https://regexr.com/">https://regexr.com/</a> for more. Do note that the filter uses
                    javascript, which does not support all regex options.</p>
            </details>
        </details>
        <details class="pmf__info-section">
            <summary>Examples</summary>
            <h5>Example 1</h5>
            <table class="pmf__matching-examples">
                <thead>
                    <tr>
                        <th scope="col">We want…</th>
                        <th scope="col">setting</th>
                        <th scope="col">search term(s)</th>
                        <th scope="col">N</th>
                        <th scope="col">matches</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>Fics with Leia in the first 3 character tags</td>
                        <td>Include Characters</td>
                        <td>leia</td>
                        <td>3</td>
                        <td>Leia, Leia Organa, Leia Skywalker</td>
                    </tr>
                    <tr>
                        <td>…that don't have Luke or Han as the main characters (it's fine if they're in second
                            place)
                        </td>
                        <td>Exclude Characters</td>
                        <td>luke skywalker, han solo</td>
                        <td>2</td>
                        <td></td>
                    </tr>
                </tbody>
            </table>
            <h5>Example 2:<br></h5>
            <table class="pmf__matching-examples">
                <thead>
                    <tr>
                        <th scope="col">We want…</th>
                        <th scope="col">setting</th>
                        <th scope="col">search term(s)</th>
                        <th scope="col">N</th>
                        <th scope="col">matches</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>relationships with Avon</td>
                        <td>Include Rels.</td>
                        <td>avon</td>
                        <td>1</td>
                        <td>Jarvik / Kerr Avon, Avon &amp; Servalan...</td>
                    </tr>
                </tbody>
            </table>
            <h5>Example 3 (with *wildcards):<br></h5>
            <table class="pmf__matching-examples">
                <thead>
                    <tr>
                        <th scope="col">We want…</th>
                        <th scope="col">setting</th>
                        <th scope="col">search term(s)</th>
                        <th scope="col">N</th>
                        <th scope="col">matches</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td><em>platonic</em> relationships with Avon</td>
                        <td>Include Rels.</td>
                        <td>avon</td>
                        <td>1</td>
                        <td>Jarvik / Kerr Avon, Avon &amp; Servalan...</td>
                    </tr>
                    <tr>
                        <td></td>
                        <td>Exclude Rels.</td>
                        <td>/*avon, avon*/</td>
                        <td>1</td>
                        <td>relationships containing "Avon" but not "/"</td>
                    </tr>
                </tbody>
            </table>
            <h5>Example 4 (with regex): <br>
                you don't want to read fics focusing on original
                characters, but don't mind if they're part of the story.</h5>
            <table class="pmf__matching-examples">
                <thead>
                    <tr>
                        <th scope="col">We want…</th>
                        <th scope="col">setting</th>
                        <th scope="col">search term(s)</th>
                        <th scope="col">N</th>
                        <th scope="col">matches</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>to HIDE fics with OCs in first or second place</td>
                        <td>Exclude Characters</td>
                        <td>original.*character, O[FM]?C</td>
                        <td>1</td>
                        <td>Original Characters, Original Female Character, OC, OMC…</td>
                    </tr>
                </tbody>
            </table>
            <h5>Example 5a (with *wildcards)</h5>
            <table class="pmf__matching-examples">
                <thead>
                    <tr>
                        <th scope="col">We want…</th>
                        <th scope="col">setting</th>
                        <th scope="col">search term(s)</th>
                        <th scope="col">N</th>
                        <th scope="col">matches</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>Katara/... as the first ship</td>
                        <td>Include Rels.</td>
                        <td>/*katara, katara*/</td>
                        <td>1</td>
                        <td>Aang / Katara, Katara / Toph, Katara / Zuko...</td>
                    </tr>
                    <tr>
                        <td>...EXCEPT Katara/Aang (okay in the background)</td>
                        <td>Exclude Rels.</td>
                        <td>aang/katara, katara/aang</td>
                        <td>2</td>
                        <td>Aang / Katara, Katara / Aang</td>
                    </tr>
                </tbody>
            </table>
            <p>Wait! In some fandoms this would work, but Avatar
                uses "Kataang", "Zutara", etc. <span class="search-term">"kat*, *ara"</span> would match those but
                also,
                e.g., "<span class="str-match">Kat</span>ara &amp; Sokka". <br>
                We could list them all, or...
            </p>
            <h5>Example 5b (with regex)</h5>
            <table class="pmf__matching-examples">
                <thead>
                    <tr>
                        <th scope="col">We want…</th>
                        <th scope="col">setting</th>
                        <th scope="col">search term(s)</th>
                        <th scope="col">N</th>
                        <th scope="col">matches</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>Katara/... as the first ship </td>
                        <td>Include Rels.</td>
                        <td>/.*katara, katara.*/, ^kat[a-z]+$, ^[a-z]+tara$</td>
                        <td>1</td>
                        <td>as in 5a + "Kat..." followed by any letters without spaces + "...tara".</td>
                    </tr>
                    <tr>
                        <td>...EXCEPT Katara/Aang (fine if it's in the background)</td>
                        <td>Exclude Rels.</td>
                        <td>aang / katara, katara / aang</td>
                        <td>2</td>
                        <td>as in 5a</td>
                    </tr>
                </tbody>
            </table>
        </details>
    </div>`

    // add collapsible menu directly above AO3's filter sidebar. Get DOM objects.
    if (!ao3FilterSidebar) { return }
    ao3FilterSidebar.insertAdjacentHTML('afterbegin', sidebarHTML)
    const filterMenu = {
        container: $('.pmf__sidebar-head'),
        expander: $('.pmf__sidebar-head .expander'),
        toggle: $('#pmf__filter-toggle'),
    }
    const settingsMenu = {
        container: $('.pmf__config-head'),
        expander: $('.pmf__config-head .expander'),
        AO3sav: $('#pmf__setting-AO3-sav'),
        debugMode: $('#pmf__setting-debug'),
    }
    const textFields = $$('.pmf__tag-block :is(input[type="text"], textarea)'),
        checkboxFields = $$('.pmf__tag-block input[type="checkbox"]')

    // DEFINE FILTER FIELDS + GETTERS
    class tagBlock { // set elements, get values. If the checkbox is unselected, disable the other fields.
        constructor(includeOrExclude, tagType, storedVals) {
            const tagBlock = $(`.pmf__tag-block.${includeOrExclude}.${tagType}`)
            this.defaultMatchResult = (includeOrExclude === 'include') // match = true for includes, match = false for excludes
            this.checkboxField = tagBlock.querySelector('input[type=checkbox]')
            this.textareaField = tagBlock.querySelector('.pmf__tag-list')
            this.tagLimitField = tagBlock.querySelector('.pmf__within input')
            this.checkboxField.addEventListener('change', () => {
                const tagBlockEnabled = this.checkboxField.checked
                this.textareaField.readOnly = !tagBlockEnabled
                this.tagLimitField.readOnly = !tagBlockEnabled
            })

            if (!storedVals) { return }
            this.checkboxField.checked = storedVals.check
            this.textareaField.value = storedVals.pattern
            this.tagLimitField.value = storedVals.tagLim
            this.checkboxField.dispatchEvent(new Event('change')) // apply formatting
        }
    }

    class currentFilter { // store the filter values at the time the object is created
        constructor(tagBlock) {
            this.check = tagBlock.checkboxField.checked
            this.pattern = tagBlock.textareaField.value.split(',').map(s => removeDiacriticsAndExtraSpaces(s))
            this.tagLim = tagBlock.tagLimitField.value.trim()
            this.isValid = (this.pattern.length > 0 && this.tagLim.length > 0) // ok to save
            this.checkTags = (this.check && this.isValid) // ok to commit
            this.defaultMatchResult = tagBlock.defaultMatchResult
        }
    }

    // SET INITIAL VALUES ------------------------------------------------------------
    // Menu
    settingsMenu.AO3sav.checked = stored.ao3SaviorIsInstalled
    settingsMenu.debugMode.checked = stored.debugMode
    toggleExpand(filterMenu, stored.menuIsExpanded)
    let filterIsOn = stored.filterIsOn
    setFilterStatus()

    // Define & populate filter fields
    const characterBlock = new tagBlock('include', 'characters', stored.characters),
        relationshipBlock = new tagBlock('include', 'relationships', stored.relationships),
        excludedCharacterBlock = new tagBlock('exclude', 'characters', stored.excludedCharacters),
        excludedRelationshipBlock = new tagBlock('exclude', 'relationships', stored.excludedRelationships)
    $(`.pmf__syntax input[value=${stored.format}]`).checked = true

    // Filter on load: add 20ms delay to prevent conflicts with AO3 savior (which runs after a 15ms delay)
    if (filterIsOn) {
        const delay = stored.ao3SaviorIsInstalled ? 20 : 0
        setTimeout(applyFilters, delay)
    }

    // ADD EVENT LISTENERS ----------------------------------------------------------
    // (expand controls, toggle filters off/on, apply/clear filters ... and save)

    filterMenu.expander.addEventListener('click', () => {
        const isExpanded = toggleExpand(filterMenu)
        GM_setValue('isExpanded', isExpanded)
    })
    settingsMenu.expander.addEventListener('click', () => { toggleExpand(settingsMenu) }) // collapsed by deafult
    filterMenu.toggle.addEventListener('click', toggleFilterStatus)
    settingsMenu.AO3sav.addEventListener('change', function () { GM_setValue('ao3SaviorIsInstalled', this.checked) })
    settingsMenu.debugMode.addEventListener('change', function () {
        debugModeOn = this.checked
        GM_setValue('debugMode', debugModeOn)
    })

    // BUTTON: info/help popup
    for (const infoButton of $$('.pmf__ui .question')) {
        infoButton.addEventListener('click', openAo3Modal)
    }

    // BUTTON: Apply filters. If filters are off, turn them on.
    $('.pmf__apply').addEventListener('click', () => {
        if (!filterIsOn) { toggleFilterStatus() }
        const thisFilter = applyFilters()
        saveFilterFields(...thisFilter)
    })

    // BUTTON: Clear filters
    $('.pmf__ui .footnote a').addEventListener('click', () => {
        for (const field of textFields) { field.value = field.defaultValue }
        for (const field of checkboxFields) {
            field.checked = false
            field.dispatchEvent(new Event('change'))
        }
        showAllWorks()
        GM_deleteValues(['characters', 'relationships', 'excludedCharacters', 'excludedRelationships'])
    })

    // ------------------------------------------------------------------------------------
    // collapse/expand controls
    function toggleExpand(target, ...forceExpand) {
        const expanded = target.container.classList.toggle('expanded', forceExpand[0])
        target.container.classList.toggle('collapsed', !expanded)
        target.expander.setAttribute('aria-expanded', expanded)
        return expanded
    }

    // toggle filters off/on
    function toggleFilterStatus() {
        if (noFilterYet) { applyFilters() }
        filterIsOn = !filterIsOn
        setFilterStatus()
    }
    function setFilterStatus() {
        GM_setValue('filterIsOn', filterIsOn) // store value
        workslistContainer.classList.toggle('show-priority-filters', filterIsOn) // disable the CSS which hides stories

        // format the toggle button
        filterMenu.toggle.setAttribute('aria-pressed', filterIsOn)
        filterMenu.toggle.classList.toggle('current', filterIsOn)
        filterMenu.toggle.textContent = filterIsOn ? 'On' : 'Off'
    }

    function showAllWorks() {
        for (let i = 0; i < works.length; i++) {
            works[i].classList.toggle('pmf__work', false)
        }
    }

    function saveFilterFields(chars, rels, xChars, xRels) {
        [
            ['characters', chars], ['relationships', rels], ['excludedCharacters', xChars], ['excludedRelationships', xRels],
        ].forEach(([settingName, tagSet]) => {
            GM_setValue(settingName, { check: tagSet.check, pattern: tagSet.pattern, tagLim: tagSet.tagLim })
        })
        GM_setValue('format', $('.pmf__syntax input[name="format"]:checked').value)
    }

    // Hide works which don't prioritise your characters/relationships. Return the values of the current filter for saving.
    function applyFilters() {

        noFilterYet = false

        // Retrieve filter values
        const format = $('.pmf__syntax input[name="format"]:checked').value,
            characters = new currentFilter(characterBlock),
            relationships = new currentFilter(relationshipBlock),
            excludedCharacters = new currentFilter(excludedCharacterBlock),
            excludedRelationships = new currentFilter(excludedRelationshipBlock)
        const thisFilter = [characters, relationships, excludedCharacters, excludedRelationships]
        debugLog(`thisFilter = ${JSON.stringify(thisFilter)}`)

        // If no valid characters/relationships are found, exit early (and reveal all)
        if (!characters.checkTags && !relationships.checkTags && !excludedCharacters.checkTags && !excludedRelationships.checkTags) {
            showAllWorks()
            debugLog('No valid filters found!')
            return thisFilter
        }

        // iterate through works
        for (let i = 0; i < works.length; i++) {

            // Get first n relationships/characters and check if any are in the user settings
            const firstNchars = getFirstNTags(works[i], '.characters', characters, excludedCharacters),
                firstNrels = getFirstNTags(works[i], '.relationships', relationships, excludedRelationships),
                charMatch = matchTags(characters, firstNchars, format),
                relMatch = matchTags(relationships, firstNrels, format),
                xCharMatch = matchTags(excludedCharacters, firstNchars, format),
                xRelMatch = matchTags(excludedRelationships, firstNrels, format)

            // Show work if it prioritises your tags and none of the blacklisted tags. Otherwise, hide it.
            const workIsValid = relMatch && charMatch && !xRelMatch && !xCharMatch
            debugLog(`firstNchars = ${firstNchars && firstNchars.join(', ')} || firstNrels = ${firstNrels && firstNrels.join(', ')}
    workIsValid = ${workIsValid}:
    relMatch = ${relMatch}, charMatch = ${charMatch}, xRelMatch = ${xRelMatch}, xCharMatch = ${xCharMatch}`)
            works[i].classList.toggle('pmf__work', !workIsValid)
            if (workIsValid) { continue }

            // If AO3 savior hid the work, add warning <span> to its fold element, then continue to the next work.
            if (stored.ao3SaviorIsInstalled && works[i].classList.contains('ao3-savior-work')) {
                if (!works[i].querySelector('.pmf__reason-for-ao3-sav')) {
                    const pmfReason = '<span class = "pmf__reason-for-ao3-sav">; does not prioritise your tags</span>'
                    const ao3savBlockedTag = works[i].querySelector('.ao3-savior-reason strong')
                    ao3savBlockedTag.insertAdjacentHTML('afterend', pmfReason)
                }
                continue
            }

            // Add explanation and "show work" button, if it does not already exist. If it does, hide by default.
            let fold = { container: works[i].querySelector('.pmf__fold'), get btn() { return fold.container?.querySelector('.pmf__fold-btn') } }
            if (!fold.container) { fold = createFold(works[i]) }
            toggleHideWork(fold, true)
        }
        return thisFilter
    }

    // Get the first N tags (where N = largest of the two tag limits). Remove diacritics.
    function getFirstNTags(work, tagClassSelector, includedTagSet, excludedTagSet) {
        const checkTags = (includedTagSet.checkTags || excludedTagSet.checkTags)
        return checkTags && [...work.querySelectorAll(tagClassSelector)]
            .slice(0, Math.max(includedTagSet.tagLim, excludedTagSet.tagLim))
            .map(tag => removeDiacriticsAndExtraSpaces(tag.textContent))
    }

    // Check if the selected tags match the given filter
    function matchTags(tagSet, tagsToCheck, format) {
        if (!tagSet.checkTags) { return tagSet.defaultMatchResult } // show work (TRUE) for included tags, hide (FALSE) for excluded
        tagsToCheck = tagsToCheck.slice(0, tagSet.tagLim)
        for (const userTag of tagSet.pattern) {
            let pattern = removeDiacriticsAndExtraSpaces(userTag)
            pattern = (format === 'wildcard') ? wildcardPattern(userTag) : userTag // FIX magic string
            const rx = RegExp(pattern, 'gi')
            for (const workTag of tagsToCheck) {
                if (rx.test(workTag)) { return true }
            }
        }
        return false
    }

    // Format wildcard * search pattern (escaping all other special characters)
    function wildcardPattern(pattern) {
        return '\\b'
            + pattern.replaceAll(/[.+?^=!:${}()|[\]/\\]/g, '\\$&').replaceAll('*', '.*')
            + '\\b'
    }

    // Remove diacritics (this will not affect actual letters like ñ) and extra spaces
    // https://www.davidbcalhoun.com/2019/matching-accented-strings-in-javascript/
    function removeDiacriticsAndExtraSpaces(str) {
        return str
            .normalize('NFD')
            .replace(/[\u0300-\u036f]/gi, '')
            .replace(/[\s\n]{2,}/g, ' ')
            .trim()
    }

    // Mimic AO3 savior fold (not an exact copy: AO3 savior wraps the work blurb in a div)
    function createFold(thisWork) {
        const fold = {
            container: createNewElement('div', 'pmf__fold'),
            note: createNewElement('span', 'pmf__fold-note', 'This work does not prioritise your preferred tags.'),
            reason: createNewElement('span', 'pmf__hide-reason'),
            btn: createNewElement('button', 'pmf__fold-btn', 'Show'),
        }

        fold.container.append(fold.note, fold.reason, fold.btn)
        thisWork.prepend(fold.container)
        fold.btn.addEventListener('click', () => { toggleHideWork(fold) })
        return fold
    }

    function toggleHideWork(fold, forceToggle) {
        const isHidden = fold.container.classList.toggle('pmf__hidden', forceToggle)
        fold.btn.textContent = isHidden ? 'Show' : 'Hide'
    }

    // AO3 help/info modal: manually replicate the open event, allow AO3 to handle closing
    const ao3Modal = {
        bg: $('#modal-bg'),
        loading: $('#modal-bg .loading'),
        wrapper: $('#modal-wrap'),
        window: $('#modal'),
        content: $('#modal .userstuff'),
        closeBtn: $('#modal .action.modal-closer'),
    }

    function openAo3Modal() {
        debugLog('attempting to open modal...')
        ao3Modal.content.insertAdjacentHTML('afterbegin', infoModalHTML) // add content
        ao3Modal.window.querySelector('.title').textContent = 'Tag priority filters' // select each time: I think AO3 rebuilds this element on close
        window.addEventListener('keydown', closeAo3Modal)

        // CSS: replicate AO3's inline styles. The default close event clears them.
        const scrollbarWidth = `${window.innerWidth - document.body.clientWidth}px`
        for (const [el, ruleset] of [ // eslint-disable-next-line @stylistic/quote-props
            [document.body, { 'margin-right': scrollbarWidth, overflow: 'hidden', height: '100vh' }], // prevent scrolling!
            [ao3Modal.bg, { display: 'block', opacity: 0, transition: 'opacity 150ms ease-in' }],
            [ao3Modal.wrapper, { display: 'block', opacity: 0, transition: 'opacity 150ms ease-in', top: `${window.scrollY}px` }], // position on page
        ]) {
            Object.assign(el.style, ruleset)
        }
        ao3Modal.loading.style.display = 'none'

        setTimeout(() => {
            for (const el of [ao3Modal.bg, ao3Modal.wrapper, ao3Modal.window]) { el.style.opacity = 1 }
            ao3Modal.window.classList.add('tall')
        }, 0)
        setTimeout(() => {
            for (const el of [ao3Modal.bg, ao3Modal.wrapper]) {
                el.style.removeProperty('transition')
                el.style.removeProperty('opacity')
            }
        }, 300)
    }

    function closeAo3Modal(e) { // and remove event listener if the modal is hidden. Bit of a // HACK.
        const modalIsOpen = (ao3Modal.bg.style.display != 'none')
        if (!modalIsOpen || e.key === 'Escape') { window.removeEventListener('keydown', closeAo3Modal) }
        if (modalIsOpen && e.key === 'Escape') { ao3Modal.closeBtn.click() }
    }

    function createNewElement(elementType, className, textContent) {
        const el = document.createElement(elementType)
        el.className = className
        el.textContent = textContent
        return el
    }

    const hiderCss = `
    .pmf__fold, .ao3-sav-pmf__reason {
        display: none
    }
    .pmf__work {
        & > .pmf__fold {
            align-items: center;
            display: flex;
            justify-content: flex-start;
            & .pmf__fold-btn { margin-left: auto; }
        }
        & > .ao3-sav-pmf__reason { /* span inserted in AO3 savior text */
            display: inherit
        }
        & > .pmf__hidden ~ * { 
            display: none;
        }
        & > .pmf__fold:not(.pmf__hidden) {
            border-bottom: 1px dashed;
            margin-bottom: 15px;
            padding-bottom: 5px;
        }
    }
    ol.work:not(.show-priority-filters) > .pmf__work > * {
        display: inherit;
        &.pmf__fold { display: none }
    }`

    GM_addStyle(hiderCss + `.pmf__ui {
    font-size: 0.9em;

    & h3, h4, dt, dd {
        margin: unset;
    }
    & button {
        margin: 0.15em 0;
    }

    /* SIDEBAR */

    &.pmf__sidebar {
        background-color: antiquewhite;
        padding: 0.643em;
    }
    & .pmf__sidebar-head {
        display: flex;
        justify-content: space-between;
        & .expander {
            font-size: 1.2em;
        }
        & #pmf__filter-toggle {
            width:2.5em;
            &.current {
                font-weight: 700;
            }
            &:hover, &:focus-visible {
                color: #900;
                border-top: 1px solid #999;
                border-left: 1px solid #999;
                box-shadow: inset 2px 2px 2px #bbb;
            }
        }
    }

    & .pmf__wrap {
        margin-top: 1.3em;
        & > .pmf__head {
            padding: 0.1em;
            border-bottom: solid 2px firebrick;
        }
    }        
    & .pmf__tag-block, & .pmf__wrap .pmf__head {
        margin-bottom: 0.4em;
    }
    & .pmf__syntax {
        margin-top: 2em;
    }
    & .pmf__apply {
        margin: 1em 0;
        &::before { content: "🡆\\00a0" } /*00a0 for nbsp, slash escaped*/
    }
    & .pmf__config-head {
        display: flex;
        justify-content: space-between;
        & .expander {
            font-size: 1.1em;
        }
        & .footnote {
            min-width: fit-content;
        }
        & + .expandable {
            background-color: #FCF5EB;
            box-shadow: inset 0px 7px 7px -7px #999;
            padding: 1em 0.5em;
            box-sizing: border-box;
            display: grid;
            row-gap: 0.5em;
            & #pmf__setting-debug + span + span::before { content: "🐞"; margin-right: 0.3em; }
            & .save::before { content: "💾" ; float:left }
            & .load::before { content: "🠋" ; text-decoration: underline; float:left}
            & .actions { margin-top: 0.5em;}
        }
    }

    /* MENU ELEMENTS */

    & dt.collapsed + dd.expandable {
        display: none;        
    }
    & textarea:read-only, input[type=text]:read-only {
        background-color: #FCF5EB;
        color: #525252;
    }
    & .pmf__tag-list {
        resize: vertical;
        width: 100%;
        box-sizing: border-box;
        min-height: unset;
        margin: 0.25em 0 0.35em 0;
        padding: 0.3em;
        font-family: monospace;
    }
    & .pmf__within {
        display: block;
        text-align: right;
        & .pmf__tag-lim {
            width: 1.3em;
            height: 1.3em;
	        text-align: center;
        }
    }
    & fieldset {
        margin: 0 0 0.6em 0;
        box-sizing: border-box;
        width: 100%;
        box-shadow: inset 0 1px 2px #ccc; /*mimic AO3 textboxes*/
        background-color: #FCF5EB;
        & .question {
            width: unset;
            font-size: 1em;
            vertical-align:text-top;
            float: right;
        }
    }
    & label {
        white-space: nowrap;
        margin-right: unset;
    }
    & .pmf__explanatory-text {
        display: block;
        font-size: 0.8em;
        margin-left: 2em;
        line-height: 1.1em;
        color: #525252;
    }
    & .actions button {
        box-sizing: border-box;
        width: 100%;
        height: auto;
    }
    & .question {
        padding:0 0.55em;
        margin: 0 1px;
        background: #d1e1ef;
        color: #2a547c;
        border: 1px solid #2a547c;
        border-radius: 0.75em;
        box-shadow: -1px -1px 2px rgba(0,0,0,0.25);
        font: bold 0.75em Georgia, serif;
        vertical-align: super;
        cursor: help;
    }
    & .footnote {
        padding-right: unset;
    }

    /* MODAL POPUP */

    &.popup {
        font-size: 1em;
        & .pmf__search-term {
            font-family: 'Courier New', Courier, monospace;
        }
        & .pmf__str-match {
            text-decoration: underline;
        }
        & > details {
            margin: 0.9em 0;
            & > :last-child {
                margin-bottom: 2em; /*collapsible spacing*/
            }
            & > summary {
                border-bottom: solid 2px firebrick;
                font-family: Georgia, serif;
                font-size: 1.15em;
                line-height: 1.5em;
                font-weight: 700;
            }
        }
        & summary {
            cursor: default;
        }
        & .pmf__rx-cheatsheet {
            padding-left: 1.5em;
            border-left: solid #dadada 4px;
            & summary {
                margin-left: -1.5em;
                background-color: #dadada;
                padding: 0.2em;
            }
            & h4 {
                margin-top: 1em;
            }
            & dl {
                display: grid;
                grid-template-columns: 6.5em 1fr;
                padding-left: 1em;
                align-items: center;
                & dt {
                    background-color: #FCF5EB;
                    font-weight: 700;
                    font-family: 'Courier New', Courier, monospace;
                    padding-left: 0.4em;
                    margin-right: 1em;
                }
            }
            & p, ul, li {
                margin: 0;
                padding: unset inherit;
            }
        }
        & li > table {
            margin: 0.5em 0; /*spacing around table*/
        }
        & table {
            table-layout: fixed;
            width: 100%;
            border-collapse: collapse;
            font-size: 0.8em;
            background-color:floralwhite;
            & th, td {
                padding: 0.4em;
            }
            &.pmf__matching-basics {
                border: 1px solid cadetblue;
                thead {
                    background-color: lightblue;
                    & th:nth-child(-n + 2) { width: 8em; } /* first two cols. N starts at 0, so -n+2: 0+2, -1+2 */
                    & th:nth-child(3) { width: 1em; }
                }
                & th:nth-child(-n + 3) { text-align: center; }
                & td:nth-child(-n + 3) { text-align: center; font: 1.1em 'Courier New', Courier, monospace; }
            }
            &.pmf__matching-examples {
                border: 1px solid firebrick;
                & th + th, td + td {
                    border-left: 1px solid palevioletred;
                }
                & thead {
                    background-color: lightpink;
                    & th:nth-child(2) { width: 7em; }
                    & th:nth-child(4) { width: 1em; }
                }
                & td:nth-child(2), td:nth-child(3), td:nth-child(4) { font: 1.1em 'Courier New', Courier, monospace; }
                & td:nth-child(4) { text-align: center; }
            }
        }
    }

}`)

    function debugLog(input) {
        if (settingsMenu.debugMode.checked) { console.log(input) }
    }

})()