// ==UserScript== // @name Ao3 Auto Bookmarker // @description Allows for autofilled bookmark summary and tags on Ao3. // @namespace Ao3 // @match http*://archiveofourown.org/works/* // @match http*://archiveofourown.org/series/* // @grant none // @version 1.2 // @author Legovil // @license MIT // @downloadURL none // ==/UserScript== // Settings for the script, allowing customization of which features are enabled. const settings = { generateTitleNote: true, generateSummaryNote: true, checkRecBox: false, checkPrivateBox: false, getRating: true, getArchiveWarnings: false, getCategoryTags: true, getFandomTags: false, getRelationshipTags: false, getCharacterTags: false, getAdditionalTags: false, generateWordCountTag: true, appendToExistingNote: false, appendToExistingTags: false, usingAo3Extensions: true, }; // Word count boundaries for generating word count tags. const wordCountBounds = [1000, 5000, 10000, 50000, 100000, 500000]; // Enum for bookmark types. const BookmarkType = Object.freeze({ WORK: 'Work', SERIES: 'Series', }); (function() { 'use strict'; // Determine the type of bookmark based on the URL. const bookmarkType = checkBookmarkType(window.location.href); // If the bookmark type is not found, cancel the generation process. if (bookmarkType === null) { console.error('Bookmark type not found. Cancelling bookmark generation.'); return; } // Generate notes and tags, and handle checkboxes based on the bookmark type. generateNotes(bookmarkType); generateTags(); handleCheckBoxes(); })(); /** * Checks the type of bookmark based on the URL. * @param {string} url The URL to check. * @return {string|null} The bookmark type or null if not found. */ function checkBookmarkType(url) { if (url.includes('/works/')) { console.log('Found Work Bookmark.'); return BookmarkType.WORK; } if (url.includes('/series/')) { console.log('Found Series Bookmark.'); return BookmarkType.SERIES; } return null; } /** * Generates notes for the bookmark. * @param {string} bookmarkType The type of bookmark. */ function generateNotes(bookmarkType) { const notesElement = document.getElementById('bookmark_notes'); if (!notesElement) { console.error('Notes element not found. Cancelling notes generation.'); return; } // Generate title and summary notes based on settings. const notes = [ settings.generateTitleNote && generateTitleNote(bookmarkType), settings.generateSummaryNote && generateSummaryNote(bookmarkType), ].filter(Boolean).join('\n\n'); // Append or replace existing notes based on settings. notesElement.value = settings.appendToExistingNote ? `${notesElement.value}\n\n${notes}` : notes; } /** * Generates the title note for the bookmark. * @param {string} bookmarkType The type of bookmark. * @return {string} The generated title note. */ function generateTitleNote(bookmarkType) { const titleQuery = bookmarkType === BookmarkType.WORK ? '.title.heading' : 'ul.series a'; const title = document.querySelector(titleQuery); if (!title) { console.warn('Title not found. Cancelling Title Note generation.'); return ''; } const bylineQuery = bookmarkType === BookmarkType.WORK ? '.byline.heading a' : '.series.meta.group a'; const byline = document.querySelector(bylineQuery); if (!byline) { console.warn('Byline not found. Cancelling Title Note generation.'); return ''; } return `${title.innerHTML.link(window.location.href)} by ${byline.outerHTML}.`; } /** * Generates the summary note for the bookmark. * @param {string} bookmarkType The type of bookmark. * @return {string} The generated summary note. */ function generateSummaryNote(bookmarkType) { const summaryQuery = bookmarkType === BookmarkType.WORK ? '.summary.module .userstuff' : '.series.meta.group .userstuff'; const summary = document.querySelector(summaryQuery); if (!summary) { console.warn('No summary found. Cancelling summary note generation.'); return ''; } return `Summary: ${summary.innerText}`; } /** * Generates tags for the bookmark. */ function generateTags() { const tagsElement = document.getElementById('bookmark_tag_string_autocomplete'); if (!tagsElement) { console.error('Tags input element not found. Cancelling bookmark tag generation.'); return; } // Remove existing tags if the setting is not to append to them. if (!settings.appendToExistingTags) { document.querySelectorAll('.added.tag a').forEach(tagLink => tagLink.click()); } console.log('Tags input element found.'); tagsElement.value = [ settings.getArchiveWarnings && getTagsFromString('warning tags'), settings.getCategoryTags && getTagsFromString('category tags'), settings.getFandomTags && getTagsFromString('fandom tags'), settings.getRelationshipTags && getTagsFromString('relationship tags'), settings.getCharacterTags && getTagsFromString('character tags'), settings.getAdditionalTags && getTagsFromString('freeform tags'), settings.generateWordCountTag && generateWordCountTag(), ].filter(Boolean).join(', '); } /** * Gets tags from a specific class name. * @param {string} tagClassName The class name to get tags from. * @return {string} The generated tags. */ function getTagsFromString(tagClassName) { const tagList = document.getElementsByClassName(tagClassName)[1]?.getElementsByClassName('tag'); if (!tagList || tagList.length === 0) { console.error(`Tags element not found. Cancelling ${tagClassName} generation.`); return ''; } return Array.from(tagList).map(tag => tag.text).join(', '); } /** * Generates a word count tag based on the word count boundaries. * @return {string} The generated word count tag. */ function generateWordCountTag() { const wordCountElement = settings.usingAo3Extensions ? document.getElementsByClassName('words')[2] : document.getElementsByClassName('words')[1]; if (!wordCountElement || wordCountElement.innerText === 'Words:') { console.error('Word count not found. Cancelling word count tag generation. Check to see if Ao3 Extensions setting is toggled correctly.'); return ''; } const wordCount = wordCountElement.innerText.replaceAll(',', '').replaceAll(' ', ''); console.log(`Word count: ${wordCount}`); let lowerBound = wordCountBounds[0]; if (wordCount < lowerBound) { return `< ${lowerBound}`; } for (const upperBound of wordCountBounds) { if (wordCount < upperBound) { return `${lowerBound} - ${upperBound}`; } lowerBound = upperBound; } return `> ${wordCountBounds[wordCountBounds.length - 1]}`; } /** * Handles the state of checkboxes based on settings. */ function handleCheckBoxes() { const recBox = document.getElementById('bookmark_rec'); if (settings.checkRecBox && recBox) { console.log('Checking rec box.'); recBox.checked = true; } const privateBox = document.getElementById('bookmark_private'); if (settings.checkPrivateBox && privateBox) { console.log('Checking private box.'); privateBox.checked = true; } }