// ==UserScript==
// @name AO3: Site Wizard
// @version 2.7
// @description Make AO3 easier to read: customize fonts and sizes across the entire site, adjust work reader margins, fix spacing issues, and configure text alignment preferences.
// @author Blackbatcat
// @match *://archiveofourown.org/*
// @license MIT
// @grant none
// @run-at document-start
// @namespace https://greasyfork.org/users/1498004
// ==/UserScript==
(function () {
"use strict";
// --- CONSTANTS ---
const FORMATTER_CONFIG_KEY = "ao3_wizard_config";
const DEFAULT_FORMATTER_CONFIG = {
paragraphWidthPercent: 70,
paragraphFontSizePercent: 100,
paragraphTextAlign: "left",
paragraphFontFamily: "",
fixParagraphSpacing: true,
paragraphGap: 1.286,
siteFontFamily: "",
siteFontWeight: "",
siteFontSizePercent: 100,
headerFontFamily: "",
headerFontWeight: "",
codeFontFamily: "",
codeFontStyle: "normal",
codeFontSize: "",
expandCodeFontUsage: false,
};
const WORKS_PAGE_REGEX =
/^https?:\/\/archiveofourown\.org\/(?:.*\/)?(works|chapters)(\/|$)/;
// --- STATE ---
let FORMATTER_CONFIG = { ...DEFAULT_FORMATTER_CONFIG };
let cachedElements = {
paraStyle: null,
siteStyle: null,
};
// --- UTILITIES ---
function getOrCreateStyle(id) {
if (!document.head) return null;
let style = document.getElementById(id);
if (!style) {
style = document.createElement("style");
style.id = id;
document.head.appendChild(style);
}
return style;
}
function loadFormatterConfig() {
try {
const saved = localStorage.getItem(FORMATTER_CONFIG_KEY);
if (saved) {
FORMATTER_CONFIG = {
...DEFAULT_FORMATTER_CONFIG,
...JSON.parse(saved),
};
}
} catch (e) {
console.error("Error loading config:", e);
}
}
function saveFormatterConfig() {
try {
localStorage.setItem(
FORMATTER_CONFIG_KEY,
JSON.stringify(FORMATTER_CONFIG)
);
} catch (e) {
console.error("Error saving config:", e);
}
}
// --- APPLY STYLES ---
function applyParagraphWidth() {
if (!cachedElements.paraStyle) {
cachedElements.paraStyle = getOrCreateStyle(
"ao3-formatter-paragraph-style"
);
if (!cachedElements.paraStyle) return;
}
if (WORKS_PAGE_REGEX.test(window.location.href)) {
const {
paragraphWidthPercent,
paragraphFontSizePercent,
paragraphTextAlign,
paragraphGap,
} = FORMATTER_CONFIG;
cachedElements.paraStyle.textContent = `
#workskin p { text-align: ${paragraphTextAlign} !important; }
${
paragraphTextAlign === "justify" || paragraphTextAlign === "left"
? `#workskin dd { text-align: ${paragraphTextAlign} !important; }`
: ""
}
${
paragraphTextAlign === "justify" || paragraphTextAlign === "left"
? `#workskin blockquote { text-align: ${paragraphTextAlign} !important; }`
: ""
}
#workskin {
max-width: ${paragraphWidthPercent}vw !important;
font-size: ${paragraphFontSizePercent}% !important;
}
#workskin p {
margin-bottom: ${paragraphGap}em !important;
}
#workskin p[align] {
text-align: ${paragraphTextAlign} !important;
}
${
paragraphTextAlign === "right"
? `
#workskin ul, #workskin ol {
direction: rtl !important;
text-align: right !important;
}
#workskin li {
text-align: right !important;
}
#workskin dl {
direction: rtl !important;
}
#workskin dt, #workskin dd {
text-align: right !important;
}
#workskin blockquote {
text-align: right !important;
}
#workskin summary {
text-align: right !important;
}
#workskin h1, #workskin h2, #workskin h3,
#workskin h4, #workskin h5, #workskin h6 {
text-align: right !important;
}
`
: ""
}
`;
// Query workskin element fresh each time
const workskin = document.getElementById("workskin");
if (workskin) {
if (paragraphTextAlign === "right") {
workskin.setAttribute("dir", "rtl");
} else {
workskin.removeAttribute("dir");
}
}
} else {
cachedElements.paraStyle.textContent = "";
}
applySiteWideStyles();
}
function applySiteWideStyles() {
if (!cachedElements.siteStyle) {
cachedElements.siteStyle = getOrCreateStyle("ao3-sitewide-style");
if (!cachedElements.siteStyle) return;
}
const {
siteFontSizePercent,
siteFontFamily,
siteFontWeight,
headerFontFamily,
headerFontWeight,
paragraphFontFamily,
codeFontFamily,
codeFontStyle,
codeFontSize,
expandCodeFontUsage,
} = FORMATTER_CONFIG;
// Build CSS more efficiently
const rules = [];
rules.push(`html { font-size: ${siteFontSizePercent}% !important; }`);
if (siteFontFamily) {
// Build selector to exclude code elements and conditionally textareas
if (expandCodeFontUsage) {
rules.push(
`body, body *:not(textarea):not(textarea *):not(code):not(pre):not(tt):not(kbd):not(samp):not(var), input:not([type="file"]), select, button:not(.comment-format button):not(ul.comment-format button) { font-family: ${siteFontFamily} !important; }`
);
} else {
rules.push(
`body, body *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var), input:not([type="file"]), textarea:not(#skin_css):not(#floaty-textarea), select, button:not(.comment-format button):not(ul.comment-format button) { font-family: ${siteFontFamily} !important; }`
);
}
}
if (siteFontWeight) {
const textareaSelector = expandCodeFontUsage
? ""
: ", textarea:not(#skin_css):not(#floaty-textarea)";
rules.push(
`body, body *, input:not([type="file"])${textareaSelector}, select, button:not(.comment-format button):not(ul.comment-format button) { font-weight: ${siteFontWeight} !important; }`
);
}
// Work font - applies to all work content except code elements, headings (when header font is set), and conditionally textareas
if (paragraphFontFamily) {
// Conditionally exclude textareas if expandCodeFontUsage is enabled (so code font can apply)
const textareaExclusion = expandCodeFontUsage ? ":not(textarea)" : "";
if (headerFontFamily) {
rules.push(
`#workskin:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6),
#workskin *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(h1 *):not(h2 *):not(h3 *):not(h4 *):not(h5 *):not(h6 *)${textareaExclusion} { font-family: ${paragraphFontFamily} !important; }`
);
} else {
rules.push(
`#workskin, #workskin *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var)${textareaExclusion} { font-family: ${paragraphFontFamily} !important; }`
);
}
}
// Header font - overrides work font
if (headerFontFamily) {
rules.push(
`h1, h1 *, h2, h2 *, h3, h3 *, h4, h4 *, h5, h5 *, h6, h6 *, .heading, .heading *,
#workskin h1, #workskin h1 *, #workskin h2, #workskin h2 *, #workskin h3, #workskin h3 *,
#workskin h4, #workskin h4 *, #workskin h5, #workskin h5 *, #workskin h6, #workskin h6 * { font-family: ${headerFontFamily} !important; }`
);
} else if (paragraphFontFamily) {
rules.push(
`#chapters h3.title,
#chapters h3.byline.heading,
.chapter .preface h3.title,
.chapter .preface h3.byline.heading,
.preface h3.title,
.preface h3.byline { font-family: ${paragraphFontFamily} !important; }`
);
}
if (headerFontWeight) {
rules.push(
`h1, h1 *, h2, h2 *, h3, h3 *, h4, h4 *, h5, h5 *, h6, h6 *, .heading, .heading *,
#workskin h1, #workskin h1 *, #workskin h2, #workskin h2 *, #workskin h3, #workskin h3 *,
#workskin h4, #workskin h4 *, #workskin h5, #workskin h5 *, #workskin h6, #workskin h6 * { font-weight: ${headerFontWeight} !important; }`
);
}
// Code fonts - apply user customizations if specified
const codeRules = [];
if (codeFontFamily)
codeRules.push(`font-family: ${codeFontFamily} !important`);
if (codeFontStyle && codeFontStyle !== "normal")
codeRules.push(`font-style: ${codeFontStyle} !important`);
if (codeFontSize) codeRules.push(`font-size: ${codeFontSize} !important`);
// Apply custom code font settings if any are specified
if (codeRules.length > 0) {
// Base code selectors for code/monospace elements - include children and nested elements
const baseCodeSelectors =
"code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, textarea#skin_css, .css.module blockquote pre, #floaty-textarea, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var *";
// Use expanded selectors if expandCodeFontUsage is enabled (adds all textareas)
const codeSelectors = expandCodeFontUsage
? "code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, textarea, textarea#skin_css, .css.module blockquote pre, #floaty-textarea, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var *, #workskin textarea"
: baseCodeSelectors;
rules.push(`${codeSelectors} { ${codeRules.join("; ")}; }`);
}
// Monospace fallback when no custom code font is specified
if (codeRules.length === 0) {
rules.push(
`code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var * { font-family: monospace !important; }`
);
if (expandCodeFontUsage) {
rules.push(
`textarea, #workskin textarea { font-family: monospace !important; }`
);
}
}
// Keep titles and bylines centered regardless of text alignment
rules.push(
`#workskin .preface .title.heading,
#workskin .preface .byline.heading,
#workskin .preface .title,
#workskin .preface .byline,
#workskin .title.heading,
#workskin .byline.heading {
text-align: center !important;
direction: ltr !important;
}`
);
// Keep code blocks left-aligned and LTR
rules.push(
`#workskin pre {
text-align: left !important;
direction: ltr !important;
}`
);
// Preserve FontAwesome icons for comment formatting toolbar
rules.push(
`ul.comment-format, ul.comment-format * { font-family: "FontAwesome", sans-serif !important; font-weight: normal !important; }`,
`ul.actions.comment-format { text-align: left !important; }`
);
cachedElements.siteStyle.textContent = rules.join("\n");
}
// --- PARAGRAPH SPACING FIX ---
const fixParagraphSpacing = (() => {
// Create closure with helper functions
function stripBrs(el, leading = true, trailing = true) {
if (leading) {
while (el.firstChild?.tagName === "BR") {
el.firstChild.remove();
}
}
if (trailing) {
while (el.lastChild?.tagName === "BR") {
el.lastChild.remove();
}
}
}
function removeEmptyElement(el) {
const content = el.textContent?.replace(/\u00A0/g, "").trim();
if (
!content &&
el.tagName !== "BR" &&
el.tagName !== "HR" &&
!el.querySelector("img, embed, iframe, video")
) {
el.remove();
}
}
function reduceBrs(userstuff) {
let el = userstuff.querySelector("br + br + br");
while (el) {
el.remove();
el = userstuff.querySelector("br + br + br");
}
}
const ALLOWED_TAGS = [
"p",
"div",
"span",
"blockquote",
"pre",
"li",
"ul",
"ol",
"table",
"tr",
"td",
"th",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
];
return function () {
if (!WORKS_PAGE_REGEX.test(window.location.href)) return;
document
.querySelectorAll(
"#workskin .userstuff:not([data-formatter-spacing-fixed])"
)
.forEach((userstuff) => {
userstuff.setAttribute("data-formatter-spacing-fixed", "true");
ALLOWED_TAGS.forEach((tag) => {
userstuff.querySelectorAll(tag).forEach((child) => {
stripBrs(child);
removeEmptyElement(child);
});
});
reduceBrs(userstuff);
});
};
})();
// --- SETTINGS MENU ---
function showFormatterMenu() {
// Remove existing dialogs
document
.querySelectorAll(".ao3-formatter-menu-dialog")
.forEach((d) => d.remove());
// Get AO3 input field background color
let inputBg = "#fffaf5";
const testInput = document.createElement("input");
document.body.appendChild(testInput);
try {
const computedBg = window.getComputedStyle(testInput).backgroundColor;
if (
computedBg &&
computedBg !== "rgba(0, 0, 0, 0)" &&
computedBg !== "transparent"
) {
inputBg = computedBg;
}
} catch (e) {}
testInput.remove();
const dialog = document.createElement("div");
dialog.className = "ao3-formatter-menu-dialog";
dialog.style.cssText = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: ${inputBg}; padding: 20px; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.2); z-index: 10000; width: 90%; max-width: 700px; max-height: 80vh; overflow-y: auto; font-family: inherit; font-size: inherit; color: inherit; box-sizing: border-box;`;
// Add CSS for the layout
const style = document.createElement("style");
style.textContent = `.ao3-formatter-menu-dialog .settings-section { background: rgba(0,0,0,0.03); border-radius: 6px; padding: 15px; margin-bottom: 20px; border-left: 4px solid currentColor; } .ao3-formatter-menu-dialog .section-title { margin-top: 0; margin-bottom: 15px; font-size: 1.2em; font-weight: bold; color: inherit; opacity: 0.85; font-family: inherit; } .ao3-formatter-menu-dialog .setting-group { margin-bottom: 15px; } .ao3-formatter-menu-dialog .setting-label { display: block; margin-bottom: 6px; font-weight: bold; color: inherit; opacity: 0.9; } .ao3-formatter-menu-dialog .setting-description { display: block; margin-bottom: 8px; font-size: 0.9em; color: inherit; opacity: 0.6; line-height: 1.4; } .ao3-formatter-menu-dialog .checkbox-label { display: block; font-weight: normal; color: inherit; } .ao3-formatter-menu-dialog input[type="text"], .ao3-formatter-menu-dialog input[type="number"], .ao3-formatter-menu-dialog select { width: 100%; box-sizing: border-box; } .ao3-formatter-menu-dialog input[type="number"]:focus { background: ${inputBg} !important; } .ao3-formatter-menu-dialog .two-column { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; } .ao3-formatter-menu-dialog .slider-with-value { display: flex; align-items: center; gap: 10px; } .ao3-formatter-menu-dialog .slider-with-value input[type="range"] { flex-grow: 1; } .ao3-formatter-menu-dialog .value-display { min-width: 40px; text-align: center; font-weight: bold; color: inherit; opacity: 0.6; } .ao3-formatter-menu-dialog .button-group { display: flex; justify-content: space-between; gap: 10px; margin-top: 20px; } .ao3-formatter-menu-dialog .button-group button { flex: 1; padding: 10px; color: inherit; opacity: 0.9; } .ao3-formatter-menu-dialog .reset-link { text-align: center; margin-top: 10px; color: inherit; opacity: 0.7; } .ao3-formatter-menu-dialog .symbol.question { font-size: 0.5em; vertical-align: middle; }`;
document.head.appendChild(style);
dialog.innerHTML = `
<h3 style="text-align: center; margin-top: 0; color: inherit;">🪄 Site Wizard Settings 🪄</h3>
<div class="settings-section">
<h4 class="section-title">📱 Site-Wide Display</h4>
<div class="setting-group">
<label class="setting-label">Base Font Size
<span class="symbol question" title="Adjust the overall text size for the entire site (percentage of browser default)"><span>?</span></span>
</label>
<div class="slider-with-value">
<input type="range" id="site-fontsize-input" min="50" max="200" step="5" value="${
FORMATTER_CONFIG.siteFontSizePercent
}">
<span class="value-display"><span id="site-fontsize-value">${
FORMATTER_CONFIG.siteFontSizePercent
}</span>%</span>
</div>
</div>
<div class="two-column">
<div class="setting-group">
<label class="setting-label" for="site-fontfamily-input">General Text Font
<span class="symbol question" title="Font for most site text"><span>?</span></span>
</label>
<input type="text" id="site-fontfamily-input" value="${
FORMATTER_CONFIG.siteFontFamily
}" placeholder="Figtree, sans-serif">
</div>
<div class="setting-group">
<label class="setting-label" for="site-fontweight-input">Font Weight
<span class="symbol question" title="Boldness of general text"><span>?</span></span>
</label>
<input type="text" id="site-fontweight-input" value="${
FORMATTER_CONFIG.siteFontWeight
}" placeholder="400, normal">
</div>
</div>
</div>
<div class="settings-section">
<h4 class="section-title">📖 Work Formatting</h4>
<div class="setting-group">
<label class="setting-label">Work Margin Width
<span class="symbol question" title="Maximum width of work reader"><span>?</span></span>
</label>
<div class="slider-with-value">
<input type="range" id="paragraph-width-slider" min="10" max="100" step="5" value="${
FORMATTER_CONFIG.paragraphWidthPercent
}">
<span class="value-display"><span id="paragraph-width-value">${
FORMATTER_CONFIG.paragraphWidthPercent
}</span>%</span>
</div>
</div>
<div class="setting-group">
<label class="setting-label">Work Font Size
<span class="symbol question" title="Size relative to site base size"><span>?</span></span>
</label>
<div class="slider-with-value">
<input type="range" id="paragraph-fontsize-slider" min="50" max="200" step="5" value="${
FORMATTER_CONFIG.paragraphFontSizePercent
}">
<span class="value-display"><span id="paragraph-fontsize-value">${
FORMATTER_CONFIG.paragraphFontSizePercent
}</span>%</span>
</div>
</div>
<div class="setting-group">
<label class="setting-label" for="paragraph-fontfamily-input">Work Font
<span class="symbol question" title="Font family for reader"><span>?</span></span>
</label>
<input type="text" id="paragraph-fontfamily-input" value="${
FORMATTER_CONFIG.paragraphFontFamily
}" placeholder="Figtree, sans-serif">
</div>
<div class="two-column">
<div class="setting-group">
<label class="setting-label" for="paragraph-align-select">Text Alignment
<span class="symbol question" title="How text is aligned within paragraphs"><span>?</span></span>
</label>
<select id="paragraph-align-select">
<option value="left" ${
FORMATTER_CONFIG.paragraphTextAlign === "left" ? "selected" : ""
}>Left Aligned</option>
<option value="justify" ${
FORMATTER_CONFIG.paragraphTextAlign === "justify"
? "selected"
: ""
}>Justified</option>
<option value="right" ${
FORMATTER_CONFIG.paragraphTextAlign === "right"
? "selected"
: ""
}>Right Aligned</option>
</select>
</div>
<div class="setting-group">
<label class="setting-label" for="paragraph-gap-input">Line Spacing
<span class="symbol question" title="Vertical space between paragraphs (multiplier). Default is 1.286."><span>?</span></span>
</label>
<input type="number" id="paragraph-gap-input" min="0" step="0.1" value="${
FORMATTER_CONFIG.paragraphGap
}">
</div>
</div>
<div class="setting-group">
<label class="checkbox-label">
<input type="checkbox" id="fix-paragraph-spacing-checkbox" ${
FORMATTER_CONFIG.fixParagraphSpacing ? "checked" : ""
}>
Fix excessive paragraph spacing
<span class="symbol question" title="Remove unnecessary blank space between paragraphs"><span>?</span></span>
</label>
</div>
</div>
<div class="settings-section">
<h4 class="section-title">🎯 Element-Specific Fonts</h4>
<div class="two-column">
<div class="setting-group">
<label class="setting-label" for="header-fontfamily-input">Header Font
<span class="symbol question" title="Font for headings (H1-H6)"><span>?</span></span>
</label>
<input type="text" id="header-fontfamily-input" value="${
FORMATTER_CONFIG.headerFontFamily
}" placeholder="Figtree, sans-serif">
</div>
<div class="setting-group">
<label class="setting-label" for="header-fontweight-input">Header Weight
<span class="symbol question" title="Boldness of header text"><span>?</span></span>
</label>
<input type="text" id="header-fontweight-input" value="${
FORMATTER_CONFIG.headerFontWeight
}" placeholder="700, bold">
</div>
</div>
<div class="setting-group">
<label class="setting-label" for="code-fontfamily-input">Code/Monospace Font
<span class="symbol question" title="Font for code blocks and preformatted text"><span>?</span></span>
</label>
<input type="text" id="code-fontfamily-input" value="${
FORMATTER_CONFIG.codeFontFamily
}" placeholder="Victor Mono Medium, monospace">
</div>
<div class="two-column">
<div class="setting-group">
<label class="setting-label" for="code-fontsize-input">Code Font Size
<span class="symbol question" title="Size relative to surrounding text"><span>?</span></span>
</label>
<input type="text" id="code-fontsize-input" value="${
FORMATTER_CONFIG.codeFontSize
}" placeholder="0.9em, 14px">
</div>
<div class="setting-group">
<label class="setting-label" for="code-fontstyle-select">Code Font Style
<span class="symbol question" title="Style for code text"><span>?</span></span>
</label>
<select id="code-fontstyle-select">
<option value="normal" ${
!FORMATTER_CONFIG.codeFontStyle ||
FORMATTER_CONFIG.codeFontStyle === "normal"
? "selected"
: ""
}>Normal</option>
<option value="italic" ${
FORMATTER_CONFIG.codeFontStyle === "italic" ? "selected" : ""
}>Italic</option>
</select>
</div>
</div>
<div class="setting-group">
<label class="checkbox-label">
<input type="checkbox" id="expand-code-font-checkbox" ${
FORMATTER_CONFIG.expandCodeFontUsage ? "checked" : ""
}>
Apply code font to comments
<span class="symbol question" title="Applies code font to all textareas. Requires a code/monospace font to be specified above."><span>?</span></span>
</label>
</div>
</div>
<div class="button-group">
<button id="formatter-save">Apply Settings</button>
<button id="formatter-cancel">Cancel</button>
</div>
<div class="reset-link">
<a href="#" id="resetFormatterSettingsLink">Reset to Default Settings</a>
</div>
`;
document.body.appendChild(dialog);
// Event delegation for sliders
dialog.addEventListener("input", (e) => {
const target = e.target;
if (target.type === "range") {
const valueId = target.id
.replace("-input", "-value")
.replace("-slider", "-value");
const valueEl = dialog.querySelector(`#${valueId}`);
if (valueEl) valueEl.textContent = target.value;
}
});
// Save button handler
dialog.querySelector("#formatter-save").addEventListener("click", () => {
const getValue = (id) => dialog.querySelector(id)?.value?.trim() || "";
const getInt = (id, def) => parseInt(getValue(id), 10) || def;
const getFloat = (id, def) => parseFloat(getValue(id)) || def;
FORMATTER_CONFIG.siteFontSizePercent = getInt(
"#site-fontsize-input",
DEFAULT_FORMATTER_CONFIG.siteFontSizePercent
);
FORMATTER_CONFIG.siteFontFamily = getValue("#site-fontfamily-input");
FORMATTER_CONFIG.siteFontWeight = getValue("#site-fontweight-input");
FORMATTER_CONFIG.paragraphWidthPercent = getInt(
"#paragraph-width-slider",
DEFAULT_FORMATTER_CONFIG.paragraphWidthPercent
);
FORMATTER_CONFIG.paragraphFontSizePercent = getInt(
"#paragraph-fontsize-slider",
DEFAULT_FORMATTER_CONFIG.paragraphFontSizePercent
);
FORMATTER_CONFIG.paragraphTextAlign =
getValue("#paragraph-align-select") ||
DEFAULT_FORMATTER_CONFIG.paragraphTextAlign;
FORMATTER_CONFIG.paragraphFontFamily = getValue(
"#paragraph-fontfamily-input"
);
FORMATTER_CONFIG.paragraphGap = getFloat(
"#paragraph-gap-input",
DEFAULT_FORMATTER_CONFIG.paragraphGap
);
FORMATTER_CONFIG.fixParagraphSpacing =
dialog.querySelector("#fix-paragraph-spacing-checkbox")?.checked ??
false;
FORMATTER_CONFIG.headerFontFamily = getValue("#header-fontfamily-input");
FORMATTER_CONFIG.headerFontWeight = getValue("#header-fontweight-input");
FORMATTER_CONFIG.codeFontFamily = getValue("#code-fontfamily-input");
FORMATTER_CONFIG.codeFontStyle = getValue("#code-fontstyle-select");
FORMATTER_CONFIG.codeFontSize = getValue("#code-fontsize-input");
FORMATTER_CONFIG.expandCodeFontUsage =
dialog.querySelector("#expand-code-font-checkbox")?.checked ?? false;
saveFormatterConfig();
dialog.remove();
applyParagraphWidth();
if (FORMATTER_CONFIG.paragraphTextAlign === "right") {
location.reload();
}
});
dialog
.querySelector("#formatter-cancel")
.addEventListener("click", () => dialog.remove());
dialog
.querySelector("#resetFormatterSettingsLink")
.addEventListener("click", (e) => {
e.preventDefault();
FORMATTER_CONFIG = { ...DEFAULT_FORMATTER_CONFIG };
saveFormatterConfig();
dialog.remove();
applyParagraphWidth();
});
}
// --- SHARED MENU MANAGEMENT ---
function initSharedMenu() {
let menuContainer = document.getElementById("scriptconfig");
if (!menuContainer) {
const headerMenu = document.querySelector(
"ul.primary.navigation.actions"
);
const searchItem = headerMenu?.querySelector("li.search");
if (!headerMenu || !searchItem) return;
menuContainer = document.createElement("li");
menuContainer.className = "dropdown";
menuContainer.id = "scriptconfig";
menuContainer.innerHTML = `
<a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a>
<ul class="menu dropdown-menu"></ul>
`;
headerMenu.insertBefore(menuContainer, searchItem);
}
const menu = menuContainer.querySelector(".dropdown-menu");
if (menu && !menu.querySelector("#opencfg_site_wizard")) {
const menuItem = document.createElement("li");
menuItem.innerHTML =
'<a href="javascript:void(0);" id="opencfg_site_wizard">Site Wizard</a>';
menuItem.querySelector("a").addEventListener("click", showFormatterMenu);
menu.appendChild(menuItem);
}
}
// --- INITIALIZATION ---
loadFormatterConfig();
console.log("[AO3: Site Wizard] loaded.");
// Apply styles with proper sequencing
function initStyles() {
if (document.head) {
applyParagraphWidth();
} else {
const observer = new MutationObserver(() => {
if (document.head) {
observer.disconnect();
applyParagraphWidth();
}
});
observer.observe(document.documentElement, { childList: true });
}
}
// Run paragraph spacing fix
function runParagraphSpacingFixIfEnabled() {
if (
FORMATTER_CONFIG.fixParagraphSpacing &&
WORKS_PAGE_REGEX.test(window.location.href)
) {
fixParagraphSpacing();
}
}
// Initialize everything
initStyles();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
runParagraphSpacingFixIfEnabled();
initSharedMenu();
});
} else {
runParagraphSpacingFixIfEnabled();
initSharedMenu();
}
})();