AO3 author+tags quick-search

Generates quick links from AO3 fics to more by the same author in the same fandom (or character/pairing/any other tag).

// ==UserScript==
// @name			  AO3 author+tags quick-search
// @version		  1.2
// @include		  https://archiveofourown.org/works*
// @include		  https://archiveofourown.org/chapters*
// @description Generates quick links from AO3 fics to more by the same author in the same fandom (or character/pairing/any other tag). 
// @namespace   rallamajoop
// ==/UserScript==

/* Adds extra links to the tags section at the top of an AO3 work page, redirecting user to any use of same tags by the same author.
* ie. a quick way to find out if the writer of the great fic you just read has written anything else for the same fandom/pairing/trope/etc.

* If printCounts is set to 'true', code will also calculate and display the number of uses of each tag in the fandom/relationship/character sections.
* This will also make the links take longer to load, as code must pre-load each link individually. To limit load, I have not implemented this for freeform 
* or other tags.

* Author-based search links are a bit of a hack, but most cases should now select correctly in the tags menu on the right. I've added a subheading identifing the tag to cover all bases.
*/

//Setting variables! Change these if you like
var printCounts = true; 		// setting this to false will prevent script for printing tag counts, which may decrease loading times and server load
var quickSearchText = "*"; 	// text for new links - change this if you want something other than an *. You can put <sup>TEXT</sup> to make it superscript
var autoSortResultsBy = ""; // valid options are "Kudos", "Word Count", "Date" or "" (none, usually defaults to date)
var importantTags = ["fandom","relationship","character"]; // these are the tags which the script prints counts for

//Proper code starts here
var loc = location.href;
var href="https://archiveofourown.org/works?utf8=%E2%9C%93&commit=Sort+and+Filter&work_search"; //[relationship_names]=";
var workPage=["https://archiveofourown.org/works/", "https://archiveofourown.org/chapters/"];
var scriptTag = "&greasemonkey"; //tag to flag that we've clicked an author quicktag link

var sortOpts = {"Kudos": "&work_search[sort_column]=kudos_count", "Word Count": "&work_search[sort_column]=word_count", "Date" : "&work_search[sort_column]=revised_at" };

if (isWorkPage(loc)) {
  // Story page, add extra links user-based search to each tag

  var h3=document.body.getElementsByClassName("byline heading");
  var authors=h3.item(0).getElementsByTagName('a');
  authors=parseAuthors(authors);

  var allTags=document.body.getElementsByClassName("work meta group").item(0);
  var dds = allTags.getElementsByTagName('dd');

  var links = dds.item(1).getElementsByTagName('a');

  for (var j=0; j<dds.length; j++) {
    var tags = dds.item(j);
    var tagType = getTagType(tags);
    if (!tagType) break;

    links = tags.getElementsByTagName('a');
    for (var i=links.length-1; i>=0; i--) {

      for (var a=0; a<authors.length; a++) {

        var author=authors[a];
        var tag = getTag(links.item(i).href);
        var newHref = buildSearch(tag, author, tagType);
        
        var isCreatedLink = tag.includes(links.item(i).href);
        var existing = document.querySelectorAll('[href="' + newHref + '"]');
        if (!isCreatedLink && existing.length==0) { //check that we aren't creating a duplicate link - can happen when page partially loads from cache 

          var text = quickSearchText;
          var newlink=document.createElement('span');
          
          if (printCounts && printCountFor(tags) && !ignoreAuthor(author)) {
            //If this is an important tag, count search results
            var count = getNumFics(newHref);
            if (count>1) {
              text = "<b><sup>(" + count.toString() + ")<sup></b>";
            }
            else {
              text = "<sup>(1)<sup></b>";
            }
          }

          var toolTip = "'" + unformatTag(tag) + "' fic by " + author;
          newlink.innerHTML = " <a href=\"" + newHref + "\" title=\"" + toolTip + "\">" + text + "</a>";

          links.item(i).parentNode.appendChild(newlink);
        }
      }
    }
  }
}


else if (loc.includes(scriptTag)) {
  // Quicktags search results page, add title to show which tag is in use

  //var sTag = getBetween("work_search[relationship_names]=", loc, "&user_id=");
  let sTag, tagType;
  [tagType, sTag] = parseLoc(loc);
  var main = document.getElementById("main");
  var heading = main.getElementsByClassName("heading").item(0);

  sTag = unformatTag(sTag);
  
  var subTitle = "<br> Author QuickSearch: " + sTag;
  var span=document.createElement('h3');
  span.innerHTML = subTitle;
  
//  const sortBys = sortLinks(loc);
//  heading.parentNode.insertBefore(sortBys, heading.nextSibling); //not really necessary as of v1.2
  heading.parentNode.insertBefore(span, heading.nextSibling);

  checkTagBox(tagType, sTag);
}

//don't bother counting fics by orphan_account - these won't all be by same author anyway
function ignoreAuthor(author) {
  if (author == "orphan_account") return true;
  return false;
}

function getTagType(element) {
  var name = element.className;
  if (!name.includes(" tags")) return false;
  name = name.replace(" tags", "");
  return name;
}

//True if work or chapter
function isWorkPage(href) {
  for (var i=0; i<workPage.length; i++) {
    if (href.includes(workPage[i])) return true;
  }
  return false;
}

//Make sure tag is selected on the right menu
function checkTagBox(tagType, tag) {
  if (tagType == "warning") tagType = "archive_warning";
  debugger
/*  let btn = document.getElementById("toggle_include_" + tagType + "_tags");
  btn = btn.getElementsByTagName("button");
  btn = btn[0];
  if (btn.getAttribute("aria-expanded") == "false") {
    btn.click();
  }*/
  const par = document.getElementById("include_" + tagType + "_tags");
  const entries = par.getElementsByTagName("label");
  for (let i=0; i<entries.length; i++) {
    let entry = entries[i];
    if (entry.textContent.includes(tag)) {
      entry.getElementsByTagName("input")[0].checked=true;
      return;
    }
  }
}


//Generate links to sort by kudos etc
function sortLinks(urlStr) {
  var sortBys=document.createElement('div');
  
  let baseUrl = urlStr.replace(scriptTag, "");
  let keys = Object.keys(sortOpts);
  let inc = [true, true, true];
  for (var i =0; i<keys.length; i++) {
    let opt = sortOpts[keys[i]];
    if (baseUrl.includes(opt)) {
      baseUrl = baseUrl.replace(opt, "");
      inc[i] = false;
    }
  }

  for (var i =0; i < keys.length; i++) {
    if(inc[i]) {
      let link=document.createElement('a');
      let key=keys[i];
      link.href=baseUrl + sortOpts[key] + scriptTag;
      link.innerHTML = "Sort by " + key;
      sortBys.append("\u2003");
      sortBys.appendChild(link);
    }
  }
  
  return sortBys;
}

//Author+tag search address
function buildSearch(tag, author, tagType="relationship"){
	var href="https://archiveofourown.org/works?utf8=%E2%9C%93&commit=Sort+and+Filter&work_search[" + tagType + "_names]="
  href+=tag + "&user_id=" + author;
  
  const sortBy = sortOpts[autoSortResultsBy];
  if (sortBy != undefined) href+= sortBy;

  href+=scriptTag;
  return href;
}

//Limit tag counts to fandom, relationship and character tags
function printCountFor(element) {
  var name = getTagType(element);
  for (var i=0; i<importantTags.length; i++) {
    if (importantTags[i] == name) return true;
  }
  return false;

}

//Retrieve page as javascript object
function getSourceAsDOM(url)
{
    var xmlhttp=new XMLHttpRequest();
    xmlhttp.open("GET",url,false);
    xmlhttp.send();
    var parser=new DOMParser();
    return parser.parseFromString(xmlhttp.responseText,"text/html");
}

//Counts results of author+tag search
function getNumFics(url) {
  var results = getSourceAsDOM(url);
  var works = results.getElementsByClassName("work index group")[0];

  if (works.children.length < 20) {
	  return works.children.length;
  }

  var h=results.getElementsByTagName("h2");
  if (h.length>0) {
    h=h[0];
	  var text = h.textContent;
    if (text.includes(" of ")) {
      text = getBetween(" of ", text, " Works");
      return text; //could convert this into an integer, but not much point
    }
  }
	return 20;
}

//May be multiple authors, may have pseudonyms. Returns as array
function parseAuthors(links) {
  var authors = [""];
  for (var i=0; i<links.length; i++) {
    var text = links.item(i).toString();
		authors[i] = getBetween("/users/", text, "/pseuds");
  }
  return authors;
}

//Retrieve tag and tag type from search string
function parseLoc(href) {
  const re=/.+&work_search\[(.+)_names\]=(.+)&user_id=.+/;
  const res = re.exec(href);
  if (res.length == 3) {
    return res.slice(1);
  }
  return ["",""];
}

//Format tag to pass to search
function getTag(href){
  var tag = getBetween("/tags/", href, "/works");
  tag = replaceAll(tag, "*s*","%2F");
  tag = replaceAll(tag, "*d*",".");
  tag = replaceAll(tag, "%20","+");
  return tag;
}

//Format tag from href for display in title
function unformatTag(tag) {
  tag = decodeURIComponent(tag);
  tag = replaceAll(tag, "*s*","/");
  tag = replaceAll(tag, "*a*","&");
  tag = replaceAll(tag, "*d*",".");
  tag = replaceAll(tag, "+", " ");
  return tag;
}


function getBetween(tag1, str, tag2) {
	var ret = str.split(tag1);
  if (ret.length<2) return "";
  ret=ret[1];
	ret = ret.split(tag2);
  if (ret.length<1) return "";
  ret = ret[0];
  return ret;
}

function replaceAll(str, search, replacement) {
	return str.split(search).join(replacement);
}