Greasy Fork is available in English.
Allows for autofilled bookmark summary and tags on Ao3.
当前为
// ==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 2.0
// @author Legovil
// @license MIT
// ==/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: true,
getCategoryTags: true,
getFandomTags: true,
getRelationshipTags: true,
getCharacterTags: true,
getAdditionalTags: true,
generateWordCountTag: true,
appendToExistingNote: false,
appendToExistingTags: true,
usingAo3Extensions: false,
};
// 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';
const bookmarkType = checkBookmarkType(window.location.href);
if (!bookmarkType) {
console.error('Bookmark type not found. Cancelling bookmark generation.');
return;
}
handleCheckBoxes();
setNotes(bookmarkType);
setTags(bookmarkType);
})();
/**
* Sets notes based on bookmark type.
* @param {string} bookmarkType - The type of the bookmark.
*/
function setNotes(bookmarkType) {
const notesElement = document.getElementById('bookmark_notes');
if (!notesElement) {
console.error('Notes element not found. Cancelling notes generation.');
return;
}
notesElement.value = generateNotes(bookmarkType, notesElement);
}
/**
* Sets tags based on bookmark type.
* @param {string} bookmarkType - The type of the bookmark.
*/
function setTags(bookmarkType) {
const tagsElement = document.getElementById('bookmark_tag_string_autocomplete');
if (!tagsElement) {
console.error('Tags input element not found. Cancelling bookmark tag generation.');
return;
}
tagsElement.value = generateTagsFromType(bookmarkType);
}
/**
* Generates tags based on the bookmark type.
* @param {string} bookmarkType - The type of the bookmark.
* @return {string} - The generated tags.
*/
function generateTagsFromType(bookmarkType) {
return (bookmarkType === BookmarkType.WORK) ? generateTags() : generateSeriesTags();
}
/**
* 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) {
const bookmarkTypes = [
{ type: '/works/', result: BookmarkType.WORK, message: 'Found Work Bookmark.' },
{ type: '/series/', result: BookmarkType.SERIES, message: 'Found Series Bookmark.' }
];
const bookmarkType = bookmarkTypes.find(({ type }) => url.includes(type));
if (!bookmarkType) {
return null;
}
console.log(bookmarkType.message);
return bookmarkType.result;
}
/**
* Generates notes for the bookmark.
* @param {string} bookmarkType The type of bookmark.
*/
function generateNotes(bookmarkType, notesElement) {
const notesArray = [
{ setting: settings.generateTitleNote, note: generateTitleNote(bookmarkType) },
{ setting: settings.generateSummaryNote, note: generateSummaryNote(bookmarkType) }
];
const notes = notesArray
.filter(noteObj => noteObj.setting)
.map(noteObj => noteObj.note)
.join('\n\n');
// Append or replace existing notes based on settings.
return 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 queries = {
WORK: { title: '.title.heading', author: '.byline.heading a' },
SERIES: { title: 'ul.series a', author: '.series.meta.group a' }
};
const query = queries[bookmarkType];
if (!query) {
console.warn(`Invalid bookmark type: ${bookmarkType}. Cancelling Title Note generation.`);
return '';
}
const { title: titleQuery, author: authorQuery } = query;
const title = document.querySelector(titleQuery);
if (!title) {
console.warn('Title not found. Cancelling Title Note generation.');
return '';
}
const author = document.querySelector(authorQuery);
if (!author) {
console.warn('Byline not found. Cancelling Title Note generation.');
return '';
}
return `${title.innerHTML.link(window.location.href)} by ${author.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 queries = {
WORK: '.summary.module .userstuff',
SERIES: '.series.meta.group .userstuff'
};
const summaryQuery = queries[bookmarkType];
if (!summaryQuery) {
console.warn(`Invalid bookmark type: ${bookmarkType}. Cancelling summary note generation.`);
return '';
}
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 series bookmark.
*/
function generateSeriesTags() {
const works = Array.from(document.querySelector(".series.work.index.group").children);
console.log(works);
if (!Array.isArray(works) || works.length === 0) {
console.warn('No works found or invalid works array. Cancelling tag generation.');
return '';
}
const tagTypes = [
{ setting: settings.getArchiveWarnings, type: 'warnings' },
{ setting: settings.getFandomTags, type: 'fandoms.heading' },
{ setting: settings.getRelationshipTags, type: 'relationships' },
{ setting: settings.getCharacterTags, type: 'characters' },
{ setting: settings.getAdditionalTags, type: 'freeforms' }
];
const tags = works.flatMap(work =>
tagTypes
.filter(tagType => tagType.setting)
.flatMap(tagType => getTagsFromString(tagType.type, work))
);
if (settings.generateWordCountTag) {
tags.push(generateWordCountTag());
}
return Array.from(new Set(tags)).join(', ');
}
/**
* Generates tags for the bookmark.
*/
function generateTags() {
// 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.');
const tagTypes = [
{ setting: settings.getArchiveWarnings, type: 'warning.tags' },
{ setting: settings.getCategoryTags, type: 'category.tags' },
{ setting: settings.getFandomTags, type: 'fandom.tags' },
{ setting: settings.getRelationshipTags, type: 'relationship.tags' },
{ setting: settings.getCharacterTags, type: 'character.tags' },
{ setting: settings.getAdditionalTags, type: 'freeform.tags' }
];
const tags = tagTypes
.filter(tagType => tagType.setting)
.map(tagType => getTagsFromString(tagType.type))
.flat();
if (settings.generateWordCountTag) {
tags.push(generateWordCountTag());
}
return tags.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, startNode = document) {
const tagList = startNode.querySelectorAll(`.${tagClassName} .tag`);
if (tagList.length === 0) {
console.error(`Tags element not found. Cancelling ${tagClassName} generation.`);
return "";
}
return Array.from(tagList, tag => tag.text);
}
/**
* Generates a word count tag based on the word count boundaries.
* @return {string} The generated word count tag.
*/
function generateWordCountTag() {
const index = settings.usingAo3Extensions ? 2 : 1;
const wordCountElement = document.getElementsByClassName('words')[index];
if (!wordCountElement || wordCountElement.innerText === 'Words:') {
console.error('Word count not found. Cancelling word count tag generation.');
return '';
}
const wordCount = wordCountElement.innerText.replaceAll(',', '').replaceAll(' ', '');
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 checkBoxSettings = [
{ setting: settings.checkRecBox, elementId: 'bookmark_rec', message: 'Checking rec box.' },
{ setting: settings.checkPrivateBox, elementId: 'bookmark_private', message: 'Checking private box.' }
];
checkBoxSettings.forEach(({ elementId, setting, message }) => {
const checkBox = document.getElementById(elementId);
if (setting && checkBox) {
console.log(message);
checkBox.checked = true;
}
});
}