// ==UserScript==
// @name RR search
// @namespace http://tampermonkey.net/
// @version 0.3.0
// @description Handy search tool for royal road fictions.
// @author Primordial Shadow
// @match *://*.www.royalroad.com/fiction/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=royalroad.com
// @grant none
// ==/UserScript==
/*
INSTRUCTIONS:
go to a fiction page in royalroad (chapter pages work too)
press "ù" to show/hide the interface
type whatever you want to search and press enter
*/
(function() {
'use strict';
// for access without hotkey
let cache = [];
let wait = false;
let ver = "v0.3.0";
let sideButton = document.createElement("img");
sideButton.setAttribute("src", "https://github.com/shadowghost69/RoyalRoad-Fiction-Analysis/blob/main/img/icon.png?raw=true");
sideButton.setAttribute("style", `
display: none;
position: fixed;
right: 0;
top: 10vh;
width: 7vw;
background-color: black;
border: solid 1px;
border-bottom-left-radius: 10% !important;
border-top-left-radius: 10% !important;
border-right-style: none !important;
padding: 2px;
cursor: pointer
`);
sideButton.setAttribute("title", "[Shortcut] -> Shift + U / ù");
sideButton.addEventListener("click", () => { root.style.left = "0px";});
sideButton.addEventListener("load", () => { sideButton.style.display = "block";});
document.body.appendChild(sideButton);
// anchor
let root = document.createElement("div");
root.setAttribute("style", "width: 100vw ;height: 100vh; display: flex;justify-content: center; align-items: center; position: fixed; top: 0; left: 100vw;z-index: 100; transition: ease-in-out 1s;");
document.body.appendChild(root);
//main container
let main = document.createElement("div");
main.setAttribute("style", `background-color: black;
width: 100%;
height: 100%;
border: 5px solid whitesmoke;
border-radius: 2rem;
display: flex;
flex-direction: column;
color:whitesmoke;
font-size: 24px;
`);
root.appendChild(main);
// interface title
let title = document.createElement("h1");
title.appendChild(document.createTextNode("RR Search"));
title.setAttribute("style", `
width: 100%;
text-align: center;
font-size: 3rem;
border-bottom: 4px solid whitesmoke;
position: relative;
height: 15%;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
flex-shrink: 0;
`);
main.appendChild(title);
let version = document.createElement("span");
version.setAttribute("style", `
font-size:1rem;
float:right;
position: absolute;
right: 2px;
bottom: 1px;`);
version.appendChild(document.createTextNode(ver));
title.appendChild(version);
// for closing without hotkey
let close = document.createElement("span");
close.setAttribute("style", `cursor: pointer;
font-size:3rem;
float:right;
position: absolute;
right: 2px;
top: 1px;`);
close.appendChild(document.createTextNode("×"));
close.addEventListener("click", ()=>{root.style.left="100vw";});
title.appendChild(close);
// sub-container
let flex = document.createElement("div");
flex.setAttribute("style", `display:flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
flex-grow: 10;
overflow: auto;`);
main.appendChild(flex);
// search bar (duh)
let searchBar = document.createElement("input");
searchBar.setAttribute("type", "text");
searchBar.setAttribute("placeholder", "Search Term");
searchBar.setAttribute("style", "max-width: 200px;color: black; text-align:center");
flex.appendChild(searchBar);
let options = document.createElement("div");
options.setAttribute("style", `width: 100%;
display:flex;
justify-content: space-around;
align-items: center;`);
let caseSense = document.createElement("div");
caseSense.innerHTML = `<input type="checkbox" id="caseSense"/> <label>Case Sensitive</label>`
options.appendChild(caseSense);
let independancy = document.createElement("div");
independancy.innerHTML = `<input type="checkbox" id="independancy" checked /> <label>Independancy</label>`
options.appendChild(independancy);
flex.appendChild(options);
// result container so i can easily clear it
let container = document.createElement("div");
container.setAttribute("style", `display:flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;`);
flex.appendChild(container);
// search trigger on pressing enter
searchBar.addEventListener("keypress", function(event) {
if (event.key === "Enter") {
event.preventDefault();
if (!wait) {
container.innerHTML= "";
Search(location.href.match(/.+fiction\/\d+\/[^\/]+/), searchBar.value);
}
}
});
// hotkeys
function handleShortcut(event) {
if (event.key === "ù" || (event.keyCode === 85 && event.shiftKey)) {
event.preventDefault();
if (root.style.left == "0px") root.style.left = "100vw";
else root.style.left = "0px";
}
}
document.addEventListener("keydown", handleShortcut);
async function fetchHTML(link) {
const response = await fetch(link);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc;
}
function getLinks(doc) {
let map = new Map();
let i = 1;
let a = new Set(doc.getElementsByTagName("td"));
let b = new Set(doc.getElementsByClassName("text-right"));
let c = a.difference(b);
for (const elem of Array.from(c)) {
let name = `(${i}) ` + elem.childNodes[1].textContent.replace(/\n | \s{2,}/g, "");
let link = elem.childNodes[1].href
map.set(name, link);
i++
}
return map;
}
/*
function getMatches(doc, word, CaseSensitive, independancy) {
let regex = new RegExp(independancy ? `.*\\b${word}\\b.*` : `.*${word}.*` ,CaseSensitive ? "g" : "gi");
let countRegex = new RegExp(independancy ? `\\b${word}\\b` : `${word}` ,CaseSensitive ? "g" : "gi");
console.debug(regex);
console.debug(countRegex);
cache.push({title: doc.getElementsByTagName("h1")[0].textContent, content: doc.getElementsByClassName("chapter-inner")[0].textContent});
try {
let title = doc.getElementsByTagName("h1")[0].textContent;
let count = doc.getElementsByClassName("chapter-inner")[0].textContent.match(countRegex).length;
let matches = doc.getElementsByClassName("chapter-inner")[0].textContent.match(regex);
return {
title: title,
count: count,
matches: matches
}
} catch (error) {
console.debug(`found no matches`)
}
}*/
function getMatchesFromCache(cacheArray, word, CaseSensitive, independancy) {
let result = [];
let regex = new RegExp(independancy ? `.*\\b${word}\\b.*` : `.*${word}.*` ,CaseSensitive ? "g" : "gi");
let countRegex = new RegExp(independancy ? `\\b${word}\\b` : `${word}` ,CaseSensitive ? "g" : "gi");
// let progress = document.createElement("h2");
// progress.appendChild(document.createTextNode(`Searching... (0/${cacheArray.length})`));
// container.appendChild(progress);
for (const obj of cacheArray) {
try {
let title = obj.title;
let count = obj.content.match(countRegex).length;
let matches = obj.content.match(regex);
result.push({
title: title,
count: count,
matches: matches
});
} catch (error) {
console.debug(`found no matches in ${obj.title}`)
}
// progress.innerText = progress.innerText.replace(progress.innerText.match(/\d+/)[0],`${+progress.innerText.match(/\d+/)[0] + 1}`);
}
// progress.remove();
return result;
}
async function getChapterData(map) {
wait = true;
let progress = document.createElement("h2");
progress.appendChild(document.createTextNode(`Fetching Chapters... (0/${Array.from(map.values()).length})`));
container.appendChild(progress);
for (const [name, link] of map) {
if (link != undefined) {
let doc = await fetchHTML(link);
cache.push({title: doc.getElementsByTagName("h1")[0].textContent, content: doc.getElementsByClassName("chapter-inner")[0].textContent});
console.debug(`${name} Fetched`)
progress.innerText = progress.innerText.replace(progress.innerText.match(/\d+/)[0],`${+progress.innerText.match(/\d+/)[0] + 1}`);
}
}
progress.remove();
wait = false;
}
async function Search(url, word) {
let result = [];
let totalCount = 0;
if (cache.length === 0 ) {
let linkMap = getLinks(await fetchHTML(url));
console.debug("links: Get");
await getChapterData(linkMap);
}
result = getMatchesFromCache(cache, word,document.getElementById("caseSense").checked, document.getElementById("independancy").checked);
for (const find of result) totalCount+= find.count;
let finalResult = {result:result, totalCount: totalCount};
console.debug(finalResult);
displayResult(finalResult, container, word);
}
function displayResult(result, parent, word) {
parent.appendChild( document.createElement("h2").appendChild(document.createTextNode(`Found ${result.totalCount} matches in total`)));
for (const match of result.result) {
let innerHTML = `<details>
<summary style="text-align: center;background: grey;
cursor: pointer;">${match.title} - ${match.count} ${match.count == 1 ? "match" : "matches"}</summary>`;
for (const para of match.matches) {
innerHTML += `<p style="font-size: 0.6em;">${para.replace(new RegExp(document.getElementById("independancy").checked ? `\\b(${word})\\b`:`(${word})` , document.getElementById("caseSense").checked ? "g" : "gi"), `<strong>$1</strong>`)}</p>`;
}
innerHTML += `</details>`;
let elem = document.createElement("div");
elem.className= "find";
elem.innerHTML = innerHTML;
parent.appendChild(elem);
}
}
})();