Greasy Fork

Greasy Fork is available in English.

AO3: Site Wizard

Change fonts and font sizes across the site easily and fix paragraph spacing issues.

当前为 2025-09-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AO3: Site Wizard
// @version      1.1.4
// @description  Change fonts and font sizes across the site easily and fix paragraph spacing issues.
// @author       Blackbatcat
// @match        http://archiveofourown.org/*
// @match        https://archiveofourown.org/*
// @license      MIT
// @grant        none
// @run-at        document-start
// @namespace http://greasyfork.icu/users/1498004
// ==/UserScript==

(function () {
  "use strict";

  // --- SETTINGS STORAGE ---
  const FORMATTER_CONFIG_KEY = "ao3_formatter_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: "",
    codeFontSize: "",
  };

  let FORMATTER_CONFIG = { ...DEFAULT_FORMATTER_CONFIG };

  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() {
  const percent = FORMATTER_CONFIG.paragraphWidthPercent;
  const fontSize = FORMATTER_CONFIG.paragraphFontSizePercent;
  const textAlign = FORMATTER_CONFIG.paragraphTextAlign;
  let fontFamily = FORMATTER_CONFIG.paragraphFontFamily;
  const gap = FORMATTER_CONFIG.paragraphGap;
  const paraStyleId = "ao3-formatter-paragraph-style";
  let paraStyle = document.getElementById(paraStyleId);
  if (!paraStyle) {
    paraStyle = document.createElement("style");
    paraStyle.id = paraStyleId;
    document.head.appendChild(paraStyle);
  }
  
  // Always apply styles to #workskin if present
  paraStyle.textContent = `
    .userstuff {
      text-align: ${textAlign || "left"} !important;
    }
    #workskin {
      max-width: ${percent || 70}vw !important;
      font-size: ${fontSize || 100}% !important;
    }
    #workskin p {
      margin-bottom: ${gap || 1.286}em !important;
      ${fontFamily ? `font-family: ${fontFamily} !important;` : ""}
    }
  `;
  
  // If right alignment is selected, set dir="rtl" on #workskin
  const workskin = document.getElementById('workskin');
  if (workskin) {
    if (textAlign === 'right') {
      workskin.setAttribute('dir', 'rtl');
    } else {
      workskin.removeAttribute('dir');
    }
  }
  
  // --- SITE-WIDE STYLES ---
  const siteStyleId = "ao3-sitewide-style";
  let siteStyle = document.getElementById(siteStyleId);
  if (!siteStyle) {
    siteStyle = document.createElement("style");
    siteStyle.id = siteStyleId;
    document.head.appendChild(siteStyle);
  }
  
  // Expanded selectors to cover more site elements
  const generalSelectors = `
    body, input, textarea, select, button,
    .toggled form, .dynamic form, .secondary, .dropdown, 
    blockquote, .prompt .blurb h6, .bookmark .user .meta, 
    a.work, span.symbol, .heading .actions, .heading .action, 
    .heading span.actions, button, span.unread, .replied, 
    span.claimed, .actions span.defaulted, .splash .news .meta, 
    .datetime, h5.fandoms.heading a.tag, dd.fandom.tags a,
    #dashboard, #header, #main, #footer,
    .navigation, .menu, .dropdown-menu,
    .blurb, .meta, .stats, .tags,
    .module, .wrapper, .region,
    li, span, div, a, p, label,
    .user, .current, .action,
    .notice, .comment, .thread,
    .work, .bookmark, .series,
    .pagination, .current
  `;
  
  const headerSelectors = `h1, h2, h3, h4, h5, h6, .heading`;
  const codeSelectors = `kbd, tt, code, var, pre, samp, textarea, textarea#skin_css, .css.module blockquote pre, #floaty-textarea`;
  
  // Build CSS with proper !important handling
  let siteStyleContent = `
    html { font-size: ${FORMATTER_CONFIG.siteFontSizePercent || 100}% !important; }
    ${generalSelectors}, ${headerSelectors} {
      ${FORMATTER_CONFIG.siteFontFamily ? `font-family: ${FORMATTER_CONFIG.siteFontFamily} !important;` : ""}
    }
    ul.comment-format, ul.comment-format * {
      font-family: FontAwesome !important;
    }
    ${generalSelectors} {
      ${FORMATTER_CONFIG.siteFontWeight ? `font-weight: ${FORMATTER_CONFIG.siteFontWeight} !important;` : ""}
    }
    ${headerSelectors} {
      ${FORMATTER_CONFIG.headerFontWeight ? `font-weight: ${FORMATTER_CONFIG.headerFontWeight} !important;` : ""}
    }
    ${codeSelectors} {
      ${FORMATTER_CONFIG.codeFontFamily ? `font-family: ${FORMATTER_CONFIG.codeFontFamily} !important;` : ""}
      ${FORMATTER_CONFIG.codeFontStyle ? `font-style: ${FORMATTER_CONFIG.codeFontStyle} !important;` : ""}
      ${FORMATTER_CONFIG.codeFontSize ? `font-size: ${FORMATTER_CONFIG.codeFontSize} !important;` : ""}
    }
  `;
  
  siteStyle.textContent = siteStyleContent;
}

  // --- PARAGRAPH SPACING FIX ---
  function fixParagraphSpacing() {
    // Helper functions
    function stripBrs(el, leading = true, trailing = true) {
      if (leading) {
        while (el.firstChild && el.firstChild.tagName === "BR") {
          el.firstChild.remove();
        }
      }
      if (trailing) {
        while (el.lastChild && el.lastChild.tagName === "BR") {
          el.lastChild.remove();
        }
      }
    }
    function removeEmptyElement(el) {
      const content = el.textContent && 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");
      }
    }

    document.querySelectorAll(".userstuff").forEach((userstuff) => {
      // Only run once per userstuff
      if (userstuff.getAttribute("data-formatter-spacing-fixed")) return;
      userstuff.setAttribute("data-formatter-spacing-fixed", "true");

      // Clean up allowed tags
      ["p", "div", "span", "blockquote", "pre", "li", "ul", "ol", "table", "tr", "td", "th", "h1", "h2", "h3", "h4", "h5", "h6"].forEach((tag) => {
        userstuff.querySelectorAll(tag).forEach((child) => {
          stripBrs(child);
          removeEmptyElement(child);
        });
      });
      reduceBrs(userstuff);
    });
  }

  // --- SETTINGS MENU ---
  function showFormatterMenu() {
    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: 900px;
      max-height: 80vh;
      overflow-y: auto;
      font-family: inherit;
      font-size: inherit;
      color: inherit;
      box-sizing: border-box;
    `;

    // Add CSS for the improved 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 .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;
      }
    `;
    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</label>
          <span class="setting-description">Adjust the overall text size for the entire site (percentage of browser default)</span>
          <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</label>
            <span class="setting-description">Font for most site text</span>
            <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</label>
            <span class="setting-description">Boldness of general text</span>
            <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="two-column">
          <div class="setting-group">
            <label class="setting-label">Work Margin Width</label>
            <span class="setting-description">Maximum width of work reader</span>
            <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">Font Size</label>
            <span class="setting-description">Size relative to site base size</span>
            <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>

        <div class="two-column">
          <div class="setting-group">
            <label class="setting-label" for="paragraph-align-select">Text Alignment</label>
            <span class="setting-description">How text is aligned within paragraphs</span>
            <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</label>
            <span class="setting-description">Vertical space between paragraphs (multiplier)</span>
            <input type="number" id="paragraph-gap-input" min="0" step="0.1" value="${FORMATTER_CONFIG.paragraphGap}">
          </div>
        </div>

        <div class="setting-group">
          <label class="setting-label" for="paragraph-fontfamily-input">Work Font</label>
          <span class="setting-description">Font family for reader</span>
          <input type="text" id="paragraph-fontfamily-input" value="${FORMATTER_CONFIG.paragraphFontFamily}" placeholder="Figtree, sans-serif">
        </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
          </label>
          <span class="setting-description">Remove unnecessary blank space between paragraphs</span>
        </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</label>
            <span class="setting-description">Font for headings (H1-H6)</span>
            <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</label>
            <span class="setting-description">Boldness of header text</span>
            <input type="text" id="header-fontweight-input" value="${FORMATTER_CONFIG.headerFontWeight}" placeholder="700, bold">
          </div>
        </div>

        <div class="two-column">
          <div class="setting-group">
            <label class="setting-label" for="code-fontfamily-input">Code/Monospace Font</label>
            <span class="setting-description">Font for code blocks and preformatted text</span>
            <input type="text" id="code-fontfamily-input" value="${FORMATTER_CONFIG.codeFontFamily}" placeholder="Victor Mono Medium, monospace">
          </div>

          <div class="setting-group">
            <label class="setting-label" for="code-fontstyle-select">Code Font Style</label>
            <span class="setting-description">Style for code text</span>
            <select id="code-fontstyle-select">
              <option value="" ${FORMATTER_CONFIG.codeFontStyle === "" ? "selected" : ""}>Normal</option>
              <option value="italic" ${FORMATTER_CONFIG.codeFontStyle === "italic" ? "selected" : ""}>Italic</option>
            </select>
          </div>
        </div>

        <div class="setting-group">
          <label class="setting-label" for="code-fontsize-input">Code Font Size</label>
          <span class="setting-description">Size relative to surrounding text</span>
          <input type="text" id="code-fontsize-input" value="${FORMATTER_CONFIG.codeFontSize}" placeholder="0.9em, 14px">
        </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);

    // Add event listeners for sliders to update values in real-time
    const sliders = [
      { slider: "site-fontsize-input", value: "site-fontsize-value" },
      { slider: "paragraph-width-slider", value: "paragraph-width-value" },
      { slider: "paragraph-fontsize-slider", value: "paragraph-fontsize-value" },
    ];

    sliders.forEach(({ slider, value }) => {
      const sliderEl = dialog.querySelector(`#${slider}`);
      const valueEl = dialog.querySelector(`#${value}`);
      if (sliderEl && valueEl) {
        sliderEl.addEventListener("input", () => {
          valueEl.textContent = sliderEl.value;
        });
      }
    });

    // Save button handler
    dialog.querySelector("#formatter-save").addEventListener("click", () => {
      // Get all values
      FORMATTER_CONFIG.siteFontSizePercent = parseInt(dialog.querySelector("#site-fontsize-input").value, 10) || DEFAULT_FORMATTER_CONFIG.siteFontSizePercent;
      FORMATTER_CONFIG.siteFontFamily = dialog.querySelector("#site-fontfamily-input").value.trim();
      FORMATTER_CONFIG.siteFontWeight = dialog.querySelector("#site-fontweight-input").value.trim();

      FORMATTER_CONFIG.paragraphWidthPercent = parseInt(dialog.querySelector("#paragraph-width-slider").value, 10) || DEFAULT_FORMATTER_CONFIG.paragraphWidthPercent;
      FORMATTER_CONFIG.paragraphFontSizePercent = parseInt(dialog.querySelector("#paragraph-fontsize-slider").value, 10) || DEFAULT_FORMATTER_CONFIG.paragraphFontSizePercent;
      FORMATTER_CONFIG.paragraphTextAlign = dialog.querySelector("#paragraph-align-select").value || DEFAULT_FORMATTER_CONFIG.paragraphTextAlign;
      FORMATTER_CONFIG.paragraphFontFamily = dialog.querySelector("#paragraph-fontfamily-input").value.trim();

      FORMATTER_CONFIG.paragraphGap = parseFloat(dialog.querySelector("#paragraph-gap-input").value) || DEFAULT_FORMATTER_CONFIG.paragraphGap;
      FORMATTER_CONFIG.fixParagraphSpacing = dialog.querySelector("#fix-paragraph-spacing-checkbox").checked;

      FORMATTER_CONFIG.headerFontFamily = dialog.querySelector("#header-fontfamily-input").value.trim();
      FORMATTER_CONFIG.headerFontWeight = dialog.querySelector("#header-fontweight-input").value.trim();
      FORMATTER_CONFIG.codeFontFamily = dialog.querySelector("#code-fontfamily-input").value.trim();
      FORMATTER_CONFIG.codeFontStyle = dialog.querySelector("#code-fontstyle-select").value;
      FORMATTER_CONFIG.codeFontSize = dialog.querySelector("#code-fontsize-input").value.trim();

      saveFormatterConfig();
      dialog.remove();
      applyParagraphWidth();
    });

    // Cancel button handler
    dialog.querySelector("#formatter-cancel").addEventListener("click", () => {
      dialog.remove();
    });

    // Reset link handler
    dialog.querySelector("#resetFormatterSettingsLink").addEventListener("click", function (e) {
      e.preventDefault();
      FORMATTER_CONFIG = { ...DEFAULT_FORMATTER_CONFIG };
      saveFormatterConfig();
      dialog.remove();
      applyParagraphWidth();
    });
  }

  // --- SHARED MENU MANAGEMENT ---
  function initSharedMenu() {
    // Create shared menu object if it doesn't exist
    if (!window.AO3UserScriptMenu) {
      window.AO3UserScriptMenu = {
        items: [],
        register: function(item) {
          this.items.push(item);
          this.renderMenu();
        },
        renderMenu: function() {
          // Find or create menu container
          let menuContainer = document.getElementById('ao3-userscript-menu');
          if (!menuContainer) {
            const headerMenu = document.querySelector("ul.primary.navigation.actions");
            const searchItem = headerMenu ? headerMenu.querySelector("li.search") : null;
            if (!headerMenu || !searchItem) return;

            menuContainer = document.createElement("li");
            menuContainer.className = "dropdown";
            menuContainer.id = "ao3-userscript-menu";
            const title = document.createElement("a");
            title.href = "#";
            title.textContent = "Userscripts";
            menuContainer.appendChild(title);
            const menu = document.createElement("ul");
            menu.className = "menu dropdown-menu";
            menuContainer.appendChild(menu);
            headerMenu.insertBefore(menuContainer, searchItem);
          }

          // Render menu items
          const menu = menuContainer.querySelector("ul.menu");
          if (menu) {
            menu.innerHTML = "";
            this.items.forEach(item => {
              const li = document.createElement("li");
              const a = document.createElement("a");
              a.href = "#";
              a.textContent = item.label;
              a.addEventListener("click", (e) => {
                e.preventDefault();
                item.onClick();
              });
              li.appendChild(a);
              menu.appendChild(li);
            });
          }
        }
      };
    }

    // Register this script's menu item
    window.AO3UserScriptMenu.register({
      label: "Site Wizard Settings",
      onClick: showFormatterMenu
    });
  }

  // --- INITIALIZATION ---
  loadFormatterConfig();
  // Apply styles immediately without waiting for DOMContentLoaded
  if (document.head) {
    applyParagraphWidth();
  } else {
    // If head doesn't exist yet, wait for it
    const observer = new MutationObserver(function(mutations) {
      if (document.head) {
        observer.disconnect();
        applyParagraphWidth();
      }
    });
    observer.observe(document.documentElement, { childList: true });
  }

  // Run fixParagraphSpacing independently on page load if enabled
  function runParagraphSpacingFixIfEnabled() {
    if (FORMATTER_CONFIG.fixParagraphSpacing) {
      fixParagraphSpacing();
    }
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', runParagraphSpacingFixIfEnabled);
  } else {
    runParagraphSpacingFixIfEnabled();
  }

  // MutationObserver to process new .userstuff elements, only after document.body exists
  function setupUserstuffObserver() {
    if (!document.body) {
      document.addEventListener('DOMContentLoaded', setupUserstuffObserver);
      return;
    }
    const userstuffObserver = new MutationObserver((mutations) => {
      if (!FORMATTER_CONFIG.fixParagraphSpacing) return;
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === 1) {
            if (node.classList.contains('userstuff')) {
              // Only process if not already fixed
              if (!node.getAttribute('data-formatter-spacing-fixed')) {
                fixParagraphSpacing();
              }
            } else {
              // Check descendants
              node.querySelectorAll && node.querySelectorAll('.userstuff').forEach((el) => {
                if (!el.getAttribute('data-formatter-spacing-fixed')) {
                  fixParagraphSpacing();
                }
              });
            }
          }
        });
      });
    });
    userstuffObserver.observe(document.body, { childList: true, subtree: true });
  }
  if (document.body) {
    setupUserstuffObserver();
  } else {
    document.addEventListener('DOMContentLoaded', setupUserstuffObserver);
  }

  // Initialize menu when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initSharedMenu);
  } else {
    initSharedMenu();
  }
})();