Creating Custom Filters on AO3 with User Scripts
I love Archive Of Our Own (AO3). The fan fiction archive, run by the Organization of Transformative Works, allows anyone to create and post original or transformative fiction without worrying about overzealous corporate lawyers. But, while the site is amazing, and it has extensive and powerful filtering system, it doesn’t offer a site-wide block function – which is important, especially if someone makes a summary so large it breaks screen readers. However, thanks to the magic of user scripts, we can add one ourselves!
What’s a User Script?
Simply put, a user script (also written as userscript) is a piece of code that lets us alter how a website works. This isn’t limited to AO3 – we can use user scripts to customize any website we want.
You can think of a website as having three basic parts1:
-
HTML – Hyper Text Markup Language – This is the data on the site. The text, tables, images, are all defined in HTML. HTML is the only thing that’s required to make a webpage – the other two items here are optional.
-
CSS – Cascading Style Sheets – While HTML defines what’s on the page, CSS defines how it looks. Fonts, colors, layout, text wraps – all that is controlled in the websites CSS.
-
Javascript – HTML and CSS alone can build a website, but HTML and CSS don’t provide interactivity. That’s where Javascript comes in – Javascript allows you to transform the HTML and CSS on a page without navigating to another one. Click a button and see something change? That’s likely Javascript.
A user script is just some extra Javascript that we place on a page – which gives us the ability to transform it in any way we want. In the case of AO3, we’re going to add some code that hides some elements if they have a specific author, or a tag (where a tag can be a fandom, an AO3 warning, a pairing, or freeform tag).
Injecting Custom Code Into a Website
User scripts are also called “Greasemonkey Scripts”, named for the Firefox browser extension Greasemonkey that first popularized them in 2005. Since then, other Greasemonkey-compatible user script managers, such as TamperMonkey and Violent Monkey, have also come onto the scene, and are available for Chrome, Safari, Edge, and Firefox on all desktop platforms (we’ll talk about mobile, and the difficulty there, later on).
For this article, I’ll be using Violent Monkey, but any one of the user script managers will work (and the code should be identical) – I believe TamperMonkey is the most popular at the moment, and some may offer nifty features like cross-browser syncing.
What we’re going to do
While this article is primarily a tutorial (designed to walk through creating the script from scratch), I know that isn’t for everyone. So this article is grouped into sections.
First, we’ll show you how to install the completed author and tag blocking script, and run it on AO3. You’ll be able to modify this script to customize what tags and authors are hidden, and if you’re feeling confident, you can modify it directly.
Then, we’ll go over creating the script, from the ground up. We’ll cover how to approach the problem, using the inspector to determine how to manipulate the page, coding up the initial author blocking code, and how we then mix in tag blocking.
Finally, we’ll cover going beyond simple tag blocking, and writing custom rules, such as blocking a work when combinations of tags are present (for example, blocking a tag, but only in a specific fandom).
Installing the Completed Script
Once you have your script manager installed on your browser, open up the settings. On Chrome using Violent Monkey, you can click the little monkey on your tool bar, and hit the gear icon.
Once there, we’re going to create a new script, so click the plus button, and select new, since we’re writing this ourselves. There will be some information at the top, but you can delete all that so you have a completely blank script.
You can then copy and paste the completed tag blocking script.
// ==UserScript==
// @name AO3Helper
// @namespace Violentmonkey Scripts
// @match https://archiveofourown.org/*
// @grant none
// @version 1.0
// @author SeanZ
// @description Customize Ao3 to avoid certain tags!
// ==/UserScript==
// get all works on the page and return an array
function getAllWorks()
{
'use strict';
return Array.from(document.getElementsByClassName("blurb"))
}
// get an array of tags from a given work
function getWorkTags(work)
{
'use strict';
// remember that tags are just links with the class "tag",
// so we can use our trusty getElementsByClassName, or
// we can use a querySelector.
let tagElements = Array.from(work.getElementsByClassName("tag"))
// to make our lives simplier, why don't we just return
// the *text* of the tag, instead of the element. The map
// function can help here - it applies a function to every
// element of an array and spits out a new array.
return tagElements.map(tagElem => tagElem.text)
}
// get an array of authors for a work
function getWorkAuthors(work) {
'use strict';
// we're just looking for links with rel=author,
// as we did earlier, but we'll also apply map on
// them too.
let authorElements = Array.from(work.querySelectorAll('a[rel=author]'))
return authorElements.map(authorElem => authorElem.text)
}
// check if two sets of things share a common element
function isAnyElementShared(listA, listB) {
'use strict';
// we're going to use the Array.some function we wrote earlier. This just
// makes it a bit more readable when we start mixing rules in our main
// function.
return listA.some(itemA => listB.includes(itemA))
}
function hideWorks()
{
'use strict';
const restrictedTags = ["TagA", "TagB", "TagC"]
const restrictedAuthors = ["AuthorA", "AuthorB_Pseud(with username)"]
// use a function to get all the works on the page
let works = getAllWorks()
// for each work...
works.forEach(work => {
// use our function getWorkTags to get all tags
// with this work
let tags = getWorkTags(work)
/// and get the authors
let authors = getWorkAuthors(work)
// use our isAnyElementShared function to check if the work contains
// any restricted tags OR (the '||' operator) a restricted author
if (isAnyElementShared(restrictedTags, tags)
|| isAnyElementShared(restrictedAuthors, authors)) {
work.style.display = 'none'
}
})
}
// run the functions!
hideWorks()
To adjust which tags and authors are blocked, simply change the text in
restrictedTags
and restrictedAuthors
- both are comma separated lists
of text (where each tag/author should be in either single or double quotes).
Go ahead and save this, and exit out of the Violent Monkey editor.
Now, reload an AO3 page, and you should see the Monkey in our toolbar has a little indicator on him.
This indicates there’s one script on that was run on this page, and works that have tags or authors on your block list will not be shown.
But how did we get here? Let’s start at the beginning!
Building from the Ground Up
To make things easier, go back to the script manager and delete your old script. Then click the ‘plus’ button again to create a new one.
This will bring up a new blank script, with some extra info filled in for you.
// ==UserScript==
// @name New script
// @namespace Violentmonkey Scripts
// @match *://*/*
// @grant none
// @version 1.0
// @author -
// @description 3/2/2021, 6:17:06 PM
// ==/UserScript==
Earlier we just copied over this, but what does this do?
This header defines script metadata – most fields don’t matter, but we will need
to set one – the @match
value – this defines which sites the script should be
run against. We want this to run on any page on AO3, so we’ll specify the full
URL of AO3, with an asterisk at the end, indicating ‘anything after here is
fine’.
That said, you might as well fill in the name and a description, just for you to remember what it is/what it does.
// ==UserScript==
// @name AO3Helper
// @namespace Violentmonkey Scripts
// @match https://archiveofourown.org/*
// @grant none
// @version 1.0
// @author SeanZWrites
// @description Customize Ao3 to avoid certain tags!
// ==/UserScript==
With this, we’re ready to add functionality to our script!
Writing an Author Block Function: What do we want to do?
We’ll start with writing author blocking – I think the code is a tad simpler than the tag-block function. But, before we write any code, we should ask ourselves: “what do we want to do – and describe it in a way that makes sense to a computer.”
Together, let’s look at a page on AO3. And see how the author information is presented.
OK – here we have three works (I went to the Hurt/Comfort tag and just grabbed these three at random). Each work has a different author.
Now, what do we want to do? Well – we want to go through each work, and for each one see if the author (that text after the ‘by’) is one of a set of names, and if so, we want that entire blurb (everything in that box) to be made invisible. But what are these things? How do we tell a computer “this is the thing I want to hide?”
We’ll use our friend the inspector! If you’re on Chrome or Firefox, you can right click on something on a page, and hit ‘Inspect’ or ‘Inspect Element’ – this will show you the HTML associated with the object, and highlight it.
So, what does this tell us? Well, this entire work blurb is contained in a
single <li>
element (“li” stands for “list item,” and if there were no CSS
styling, this would be a bullet point on a normal webpage). The list item work
has an id (work_30069789), a set of classes (work, blurb, group), and a role
(article) set on it.
Now, if we look down a little bit at what’s in this list item (you may have to hit the arrow near it to expand it), you’ll see there are more HTML elements contained. HTML nests elements, building a tree like structure, so here we have:
- Our list item (
li
)- A division (
div
) – something that doesn’t do anything on its own but is used for separating out content for styling or other uses- Header Level 4 (
h4
)- Link (
a
) – with the text of the work name - Link (
a
) – with the author name (could be more than one)
- Link (
- Header Level 4 (
- A division (
Again, ask ourselves what do we want to do? We want to look at every work, find every author link associated with a work, look at its text, and see if the text is a blocked author. If it is, we want to make the parent list item (the work blurb) go away.
But, how can we distinguish an author link from another? If we look at
the HTML again, you’ll note author links are unique! They have the
attribute (the little value with an equals sign) called rel
, and rel
is
set to ‘author’.
So our final ‘what we want to do’ is:
- Find every work blurb on the page
- For each one, every link that has a ‘rel’ attribute, and where ‘rel’ is set to ‘author’.
- Check the text contained in the link and see if the author is blocked.
- If so, hide the work!
We can code this!
Writing an Author Block Function: Coding
Our user script will be written in Javascript. For the record, I do not have the best opinion of Javascript as a programming language (it’s a weird, highly inconsistent language)2, but I’m going to put that aside for now and we’re going to get through this together. But remember, if you’re looking at this code and going “but this is weird…” take solace in the fact that you’re not alone3.
As a note, if you want to learn more about all this on your own, there a few terms you may want to know (to make searching easier), though I don’t use them in this article very much:
-
The set of HTML elements that make up the page is often referred to as the ‘document’.
-
When we are manipulating HTML (or seeking through it), we are working with the Document Object Model (the HTML DOM)
-
Individual HTML tags are called ‘Elements’.
You can see the various functions you can do to an HTML element object in Javascript on this handy reference.
To start, I’m going to show you the finished code, and then we’re going to go through it together, line by line.
function hideAuthors() {
'use strict';
// define all the authors we want to block
const blockedAuthors = ['Rainy_Summer17']
// find each work on the page. We can see each work entry is a list
// item, and the list items have the class "blurb". So lets find each one
// and place it in an array.
let works = Array.from(document.getElementsByClassName("blurb"))
// for each work on the page...
works.forEach(work => {
// find the authors
// they are the links (a elements) with rel=author
// to do this, we'll use another query selector - this is just a
// nifty way of selecting specific elements
let authorLinks = work.querySelectorAll('a[rel=author]')
// go through each author link...
authorLinks.forEach(authorLink => {
// we check if the text of the link (stuff within the <a> tag) is a blocked
// author...
if (blockedAuthors.includes(authorLink.text))
{
// and if they are, first make a note on the console we're going to a work from them
console.log("AO3Helper: Hiding work by " + authorLink.text)
//hide the work (set the display style to none)
work.style.display = 'none'
}
})
})
}
Ready? Let’s start!
'use strict';
Remember how I said Javascript is a weird, highly inconsistent language? Well,
Javascript will, quite happily, create variables out of thin air. Let’s say you
define a variable called worklist
in one place, and it’s going to store the list
of stories. But then say you forget, and call it storylist
in another? It’s
cool, Javascript will just create the other variable on the fly, and you’ll be
left wondering why storylist
is always blank. 'Use strict'
tells javascript “no,
no creating random variables. If I mistype and reference something that doesn’t
exist – create an error instead of pretending it’s fine.”
const blockedAuthors = ['Rainy_Summer17']
This is our list of blocked authors, which is sets of strings (in single or
double quotes), separated by commas. The word const
means the list is a
constant – it doesn’t change when our code executes.
let works = Array.from(document.getElementsByClassName("blurb"))
As we said earlier, our first task is to get a list of the work objects on the page. While there are quite a few list items (li elements), we can see only the work objects (the things in boxes) use the class “blurb”, so we’ll use the function getElementsByClassName to get all the work list item elements.
We also convert the output to an array which makes it easier to go through one
at a time with a forEach
.
works.forEach(work => { ... })
This is the start of our foreach loop – we’re saying “for each item in the works
array, place the current item we’re using in a variable named work
, and
execute this code.” We’ll see this a few times here!
let authorLinks = work.querySelectorAll('a[rel=author]')
Admittedly this is a bit obtuse – we want to find all the link elements (a
HTML elements) under the work object that have a rel
attribute set to
‘author’. For that, we use a “selector” – this gives us the ability to search
for elements based on their properties (this is actually the same syntax used by
CSS to select which element to style).
Note: we also could have used this instead of the earlier getElementsByClassName
function - the equivalent function for “all items with class ‘blurb’” would be
querySelectorAll('.blurb')
.
There are two similarly named functions - querySelector
and querySelectorAll
.
The former returns the first item that matches, while the second returns all
matches. Since we need to check all the authors, we’ll use the second one.
Random aside for people who are familiar with XML: you can also use XPath
expressions if you prefer with document.evaluate
– if you have no idea what
this is, ignore this!
At this point, we’ve gotten each author link for a work, but now we need
to go through each of them. Sounds like a job for another forEach
!
authorLinks.forEach(authorLink => { ... })
As we did earlier, go through each item in authorLinks
, and do something,
setting authorLink
to the one we’re currently on.
if (blockedAuthors.includes(authorLink.text))
Remember that author link is a link element, it has data about where the link
navigates to when someone clicks on it (the href
attribute). We don’t want the
element – we want what it says! That’s the link’s text
property. This code is
saying “is the link text one of the items in the blocked authors list? If so…”
console.log("AO3Helper: Hiding work by " + authorLink.text)
Javascript has a place you can output stuff that doesn’t appear on the page – this is the console. If you open up the inspector, you’ll see a tab for “Console”. You can use this to write messages to yourself and confirm the script is working! We’ll do that here and tell ourselves we blocked a work.
work.style.display = 'none'
This does exactly what you’d expect! Items on the page have a style (this is all the things CSS sets, among other things). One of the styles is display, which controls how the item is rendered on the page. You can set this to ‘none’ to make the element not be visible!
At this point, you have a working, ready to go script! We just need to call it in the script - here it is again, with all the headers, and with a call to the function added at the bottom!
// ==UserScript==
// @name AO3Helper
// @namespace Violentmonkey Scripts
// @match https://archiveofourown.org/*
// @grant none
// @version 1.0
// @author SeanZ
// @description Customize Ao3 to avoid certain tags!
// ==/UserScript==
function hideAuthors() {
'use strict';
// define all the authors we want to block
const blockedAuthors = ['Rainy_Summer17']
// find each work on the page. We can see each work entry is a list
// item, and the list items have the class "blurb". So lets find each one
// and place it in an array.
let works = Array.from(document.getElementsByClassName("blurb"))
// for each work on the page...
works.forEach(work => {
// find the authors
// they are the links (a elements) with rel=author
// to do this, we'll use another query selector - this is just a
// nifty way of selecting specific elements
let authorLinks = work.querySelectorAll('a[rel=author]')
// go through each author link...
authorLinks.forEach(authorLink => {
// we check if the text of the link (stuff within the <a> tag) is a blocked
// author...
if (blockedAuthors.includes(authorLink.text))
{
// and if they are, first make a note on the console we're going to a work from them
console.log("AO3Helper: Hiding work by " + authorLink.text)
//hide the work (set the display style to none)
work.style.display = 'none'
}
})
})
}
// run the function!
hideAuthors()
Go ahead and save this, and exit out of the Violent Monkey editor.
Now, reload an AO3 page, and you should see the Monkey in our toolbar has a little indicator on him.
Try it out for yourself: edit the blocked authors list in your script with the names of some authors on the page. Once you’re done, hit save, and then refresh the page. Works by those authors will vanish - pretty cool, huh?
If it doesn’t work - open up the console! The console will likely have an
error message telling you the line number where something was broken. For
example, if I change our line where I say blockedAuthors.includes
and
change it to blockedAuthors.includs
, and try to run the script, I’ll
see this on the console.
This tells you why the script failed, and if you look at the first line,
there is a line number - AO3Helper.user.js:36
- this means look at
line 36 of your script because that’s where a problem was.
And, for me, that’s the line with the mistyped “includes”.
OK, let’s now move onto tags!
Writing an Tag Block Function: What do we want to do?
AO3 allows a work to have many different tags - there are fandom tags, relationship tags, character tags, warning tags…
To start off, we’re going to make a function that will work with any type of tag and go from there. As we did last time, we need to ask ourself: “what do we want to do”.
Well, we can borrow some of what we did with author blocking, we know we want to:
- Go through each work.
- Look at the tags
- If the work includes specific tags, hide it.
But what are tags? To the inspector!
OK, so we can see two things here:
- Tags are simply links (
<a>
elements) with the class “tag” - Tags are children of list items (
<li>
), and the list items have a class that determines the tag type (warnings, relationships, characters, etc).
So, going back to “what do we need to do”:
- Go through each work
- Find each link that has class “tag”
- Determine if the link text is in the block list
- If so, hide the work.
We are ready… but I’m going to add one more thing. We already can go through each work on the page. And, later on, we may want to combine tag and author blocking (or even make more complex rules). So, to help with that, we’re going to break our code into functions so pieces are more reusable (and we can mix and match part later).
What’s some logical places to break things? Well, I’d say:
- Get all works on the page can be one function.
- Get all authors for a work can be one.
- Get all tags for a work can be another.
Don’t worry - like we did last time, we’ll go through this together. We’ll start with just the tag and work functions, and we’ll come back and edit the author function later.
Writing a Tag Block Function: Coding
// get all works on the page and return an array
function getAllWorks()
{
'use strict';
return Array.from(document.getElementsByClassName("blurb"))
}
// get an array of tags from a given work
function getWorkTags(work)
{
'use strict';
// remember that tags are just links with the class "tag",
// so we can use our trusty getElementsByClassName, or
// we can use a querySelector.
let tagElements = Array.from(work.getElementsByClassName("tag"))
// to make our lives easier, why don't we just return
// the *text* of the tag, instead of the element. The map
// function can help here - it applies a function to every
// element of an array and spits out a new array.
return tagElements.map(tagElem => tagElem.text)
}
// here's the final function
function hideTags()
{
'use strict';
const restrictedTags = ["Nora Reid"]
// use a function to get all the works on the page
let works = getAllWorks()
// for each work...
works.forEach(work => {
// use our function getWorkTags to get all tags
// with this work
let tags = getWorkTags(work)
// now, there are several ways we could go about this,
// but the easiest way is to use the "some" function,
// which returns true the test (what you feed it) is true
// for any element.
// here, were are saying: "if, for any tag, restricted
// tags includes that tag, then set display to none.
if (tags.some(tag => restrictedTags.includes(tag))) {
work.style.display = 'none'
}
})
}
Alright - we’re going to go a tad faster with some of this since you’ve seen some of it before, but we’ll go over everything that’s new. You got this!
Note that all functions start with ‘use strict’ (this will cut down on the number of hard-to-find errors you see).
function getAllWorks()
We define a function with the function
keyword - that gives us a name. The
parenthesis are what we pass into the function. So, this function is named
getAllWorks
and no items are passed into it.
All we do in this function is what we did in the author example - we’ve
just split it up into its own thing - it will go through the page,
find all the blurbs. The only difference is we return
what we find,
so another function can do something with the work <li>
elements.
function getWorkTags(work)
This function, getWorkTags
is given a work, and will return the
array of tags associated with the work. To do this, we’re going
to grab all the elements with class tag
(which we already know
how to do), but then we do something special!
return tagElements.map(tagElem => tagElem.text)
Meet our friend map
- it’s one of the most useful functions. Map
operates on an a set of some sort (like our collection of link elements),
and transforms them by applying some function to them. It then generates
a new set with each item changed.
So, what are we doing here? We’re taking each tag element and returning the tag’s text property (the link text). This saves us a step later on.
At this point, we have all the building blocks we need to block some tags!
Like last time, we’ll define a const
of blocked tags, but then we can just
call our functions to get works and get all the tags. The thing we need to
do here is just act on them. We do that here:
if (tags.some(tag => restrictedTags.includes(tag))) {
The some
function is true if the test provided is true for any element.
So, what we’re doing here is saying “for each tag in tags, check if it is
a member of the restricted tags list. If it is, don’t bother looking at any
more tags.”
And, after we’ve done that, we can do the same thing we did for hiding authors, we just set the style to none, and we’re done!
Here’s the full script at this point.
function hideAuthors() {
'use strict';
// define all the authors we want to block
const blockedAuthors = ['Rainy_Summer17']
// find each work on the page. We can see each work entry is a list
// item, and the list items have the class "blurb". So lets find each one
// and place it in an array.
let works = Array.from(document.getElementsByClassName("blurb"))
// for each work on the page...
works.forEach(work => {
// find the authors
// they are the links (a elements) with rel=author
// to do this, we'll use another query selector - this is just a
// nifty way of selecting specific elements
let authorLinks = work.querySelectorAll('a[rel=author]')
// go through each author link...
authorLinks.forEach(authorLink => {
// we check if the text of the link (stuff within the <a> tag) is a blocked
// author...
if (blockedAuthors.includes(authorLink.text))
{
// and if they are, first make a note on the console we're going to a work from them
console.log("AO3Helper: Hiding work by " + authorLink.text)
//hide the work (set the display style to none)
work.style.display = 'none'
}
})
})
}
// get all works on the page and return an array
function getAllWorks()
{
'use strict';
return Array.from(document.getElementsByClassName("blurb"))
}
// get an array of tags from a given work
function getWorkTags(work)
{
'use strict';
// remember that tags are just links with the class "tag",
// so we can use our trusty getElementsByClassName, or
// we can use a querySelector.
let tagElements = Array.from(work.getElementsByClassName("tag"))
// to make our lives simplier, why don't we just return
// the *text* of the tag, instead of the element. The map
// function can help here - it applies a function to every
// element of an array and spits out a new array.
return tagElements.map(tagElem => tagElem.text)
}
function hideTags()
{
'use strict';
const restrictedTags = ["Nora Reid"]
// use a function to get all the works on the page
let works = getAllWorks()
// for each work...
works.forEach(work => {
// use our function getWorkTags to get all tags
// with this work
let tags = getWorkTags(work)
// now, there are several ways we could go about this,
// but the easiest way is to use the "some" function,
// which returns true if the provided function is true
// for any element,
if (tags.some(tag => restrictedTags.includes(tag))) {
work.style.display = 'none'
}
})
}
// run the functions!
hideAuthors()
hideTags()
Making a Unified Tag and Author Block Function
We now know we can divide up tasks into functions. Instead of iterating through the page for all the works we want to block (by author), and then doing it again for blocking works by tag, let’s do it all at once.
(There is a reason I’m making us do this - it will make building custom filters easier, as you’ll see in the next section)
But how would we do this? Let’s use the same approach we did for tags - we’ll create a new function that simply takes a work and returns an author list. It will look almost identical:
// get an array of authors for a work
function getWorkAuthors(work) {
'use strict';
// we're just looking for links with rel=author,
// as we did earlier, but we'll also apply map on
// them too.
let authorElements = Array.from(work.querySelectorAll('a[rel=author]'))
return authorElements.map(authorElem => authorElem.text)
}
Also, while we can keep using the some
operator, let’s make a simple
function to improve readability - all it will do is take two lists, and
report if there are any elements shared.
function isAnyElementShared(listA, listB) {
'use strict';
// we're going to use the Array.some function we wrote earlier. This just
// makes it a bit more readable when we start mixing rules in our main
// function.
return listA.some(itemA => listB.includes(itemA))
}
We can then fold this into the same function we used for tag blocking,
and block both authors and tags at the same time! We’ll also
use our first logical operator - ||
- which means “or”.
// ==UserScript==
// @name AO3Helper
// @namespace Violentmonkey Scripts
// @match https://archiveofourown.org/*
// @grant none
// @version 1.0
// @author SeanZ
// @description Customize Ao3 to avoid certain tags!
// ==/UserScript==
// get all works on the page and return an array
function getAllWorks()
{
'use strict';
return Array.from(document.getElementsByClassName("blurb"))
}
// get an array of tags from a given work
function getWorkTags(work)
{
'use strict';
// remember that tags are just links with the class "tag",
// so we can use our trusty getElementsByClassName, or
// we can use a querySelector.
let tagElements = Array.from(work.getElementsByClassName("tag"))
// to make our lives simplier, why don't we just return
// the *text* of the tag, instead of the element. The map
// function can help here - it applies a function to every
// element of an array and spits out a new array.
return tagElements.map(tagElem => tagElem.text)
}
// get an array of authors for a work
function getWorkAuthors(work) {
'use strict';
// we're just looking for links with rel=author,
// as we did earlier, but we'll also apply map on
// them too.
let authorElements = Array.from(work.querySelectorAll('a[rel=author]'))
return authorElements.map(authorElem => authorElem.text)
}
// check if two sets of things share a common element
function isAnyElementShared(listA, listB) {
'use strict';
// we're going to use the Array.some function we wrote earlier. This just
// makes it a bit more readable when we start mixing rules in our main
// function.
return listA.some(itemA => listB.includes(itemA))
}
function hideWorks()
{
'use strict';
const restrictedTags = ["Nora Reid"]
const restrictedAuthors = ["Rainy_Summer17"]
// use a function to get all the works on the page
let works = getAllWorks()
// for each work...
works.forEach(work => {
// use our function getWorkTags to get all tags
// with this work
let tags = getWorkTags(work)
/// and get the authors
let authors = getWorkAuthors(work)
// use our isAnyElementShared function to check if the work contains
// any restricted tags OR (the '||' operator) a restricted author
if (isAnyElementShared(restrictedTags, tags)
|| isAnyElementShared(restrictedAuthors, authors)) {
work.style.display = 'none'
}
})
}
// run the functions!
hideWorks()
Everything here should look familiar, except this one line:
if (isAnyElementShared(restrictedTags, tags)
|| isAnyElementShared(restrictedAuthors, authors)) {
work.style.display = 'none'
}
What we’re saying here is “if any element of tags includes a restricted tag” OR “if any authors include a restricted author”, then hide the work.
You now have a working tag and author blocker, and this is what we had at the start of the article. But… we can go beyond this!
Custom Filtering
We have three basic logical operators that we can use to define rules:
- AND (
&&
) - OR (
||
) - NOT (
!
)
We also know how to get tags and authors on a page. And we have operators that can allow us to mix and match rules. So, let’s build a custom filter - I want to hide all work with tag “Spoilers” but only if the work is in the Minecraft fandom (which is the “Minecraft (Video Game)” tag), or if the work is by restricted author.
The nice thing is we already know how to get tags (fandom is a tag, just like any other), and we already know how to hide works.
So, this can be simplified to:
- Get all the works (we have a function for this)
- Get the tags for each work (we have something for this too)
- See if the tags contain BOTH “Minecraft (Video Games)” and “Spoilers”
- OR see if there is a restricted author
- If either is true, hide the work.
Let’s look at our what we’re building. We’ll make this its own function.
function hideMinecraftSpoilers()
{
'use strict';
const restrictedAuthors = ["skeppyextra"]
// use a function to get all the works on the page
let works = getAllWorks()
// for each work...
works.forEach(work => {
// use our function getWorkTags to get all tags
// with this work
let tags = getWorkTags(work)
/// and get the authors
let authors = getWorkAuthors(work)
// we can make this simple since we're not looking at lists of tags,
// just specific ones!
if (tags.includes("Spoilers") && tags.includes("Minecraft (Video Game)")) {
work.style.display = 'none'
}
// this is exactly what it says on the tin! else if means "if the first condition
// wasn't true, then test..."
else if (isAnyElementShared(restrictedAuthors, authors)) {
work.style.display = 'none'
}
})
}
// run our function
hideMinecraftSpoilers()
There’s only two new things here. Let’s go over them.
if (tags.includes("Spoilers") && tags.includes("Minecraft (Video Game)")) {
work.style.display = 'none'
}
Here, we use the and operator (&&
) to combine two tests - we say if a work has both
of these tags, then set the display to none.
else if (isAnyElementShared(restrictedAuthors, authors)) { {
work.style.display = 'none'
}
Now, while we absolutely could use an OR operator and roll everything into a single
if
statement, I think this is likely easier to read for most people, and shows the
power of what you can do.
else if
is run only if the preceding if
(or else if
) wasn’t true. So with this,
you can set up chains of tests:
if (conditionA) {
doSomething()
}
else if (conditionB) {
doSomething()
}
else if (conditionC) {
doSomething()
}
This means you can mix and match various rules to block out anything you want!
At this point, we’re done, and hopefully you feel comfortable to write your own functions. We have two extra sections, but hopefully you feel prepared to better control your AO3 experience!
Addendum 1: Tag Types
In all of this article, we’ve treated all types of tags the same. There may be cases in which this isn’t what you want, and you specifically want to filter a tag, but only when it’s freeform and not a character name, for example.
This is doable.
As a simple demo, we’ll quickly code up a function that only returns freeform tags. And we’ll do this using query selectors!
Remember that tags themselves are <a>
elements, inside a <li>
element, and that
the li
class determines the type of tag? We can use this, and write a specific type
of query selector.
function getAllFreeformTags(work) {
let freeformTagElements = Array.from(work.querySelectorAll('li.freeforms > a.tag'))
// as we did in other functions, get tag text
return freeformTagElements.map(elem => elem.text)
}
What the heck is that query selector? Well, to understand that query selector, we need some information:
- A dot (
.
) in a query selector is used to select classes - The arrow (
>
) is used to indicate a parent-child relationship
So what this query selector says is “find me all a
elements, with class tag
, that are
children of a list item with the class freeforms
.
Then we do what we’ve done for other functions like this - we use map to get the text for each tag, and return an array.
You can use the inspector to see the various types of tags, and can use this to fine-tune your filtering.
Addendum 2: Mobile
Oh, if only we lived in a world where mobile browsers were as rich and featured as they are on desktop. Alas, this is not the case.
If this article was published a year ago, I would simply push you to use Firefox Mobile and we’d be done, since Firefox Mobile used to support the same add-ons as Desktop. Unfortunately, Firefox Mobile went through a redesign recently and they significantly limited the number of add-ons supported. While you can still run AdBlock, TamperMonkey (and other user script engines) currently aren’t supported.
But wait - you’re not completely out of luck. There are two mobile browsers that do still support TamperMonkey scripts - Dolphin Browser and UC Browser. While switching browsers just to run AO3 scripts is a pain, it is still possible.
You can get the mobile version of TamperMonkey for either browser from the main Tampermonkey Website.
Good luck, happy fic reading, and feel free to ping me on Twitter if you get stuck!
-
This is a simplification. Javascript devs don’t @ me. ↩︎
-
In Javascript’s defense, it’s not PHP, which is an objectively terrible language. ↩︎
-
Gary Bernhardt’s four-minute talk on Ruby and Javascript weirdness, “Wat”, is worth watching just for the humor alone. ↩︎