Danbooru Note Formatting Helper

A formatting helper toolbar for Danbooru note formatting dialogs, adding buttons to wrap highlighted text with HTML tags for easy formatting.

目前為 2025-11-02 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name Danbooru Note Formatting Helper
// @namespace http://tampermonkey.net/
// @version 1.23.0
// @description A formatting helper toolbar for Danbooru note formatting dialogs, adding buttons to wrap highlighted text with HTML tags for easy formatting.
// @author       FunkyJustin
// @license      MIT
// @match https://danbooru.donmai.us/*
// @grant none
// ==/UserScript==
(function() {
    'use strict';
    // Global try-catch wrapper to prevent errors from halting execution
    try {
        // Configuration object for easy extension - declared first to avoid hoisting issues
        const CONFIG = {
            version: '1.23.0',
            toolbarId: 'note-formatting-toolbar',
            buttons: [
                // Font Group
                {
                    id: 'font-size-btn',
                    label: 'Size',
                    title: 'Font Size (prompt px/em)',
                    action: (ta) => promptFontSize(ta)
                },
                {
                    id: 'big-btn',
                    label: 'Big',
                    title: 'Big Text (<big>)',
                    action: (ta) => applyFormat(ta, '<big>', '</big>')
                },
                {
                    id: 'small-btn',
                    label: 'Small',
                    title: 'Small Text (<small>)',
                    action: (ta) => applyFormat(ta, '<small>', '</small>')
                },
                {
                    id: 'color-btn',
                    label: 'Color ▼',
                    title: 'Text Color (click for palette/picker)',
                    action: (ta) => openColorPicker(ta, 'color')
                },
                {
                    id: 'highlight-btn',
                    label: 'Highlight ▼',
                    title: 'Text Highlight (background-color, click for palette/picker)',
                    action: (ta) => openColorPicker(ta, 'background-color')
                },
                {
                    id: 'bold-btn',
                    label: '𝐁',
                    title: 'Bold (<b>) (Ctrl+B)',
                    action: (ta) => applyFormat(ta, '<b>', '</b>')
                },
                {
                    id: 'italic-btn',
                    label: '𝑰',
                    title: 'Italic (<i>) (Ctrl+I)',
                    action: (ta) => applyFormat(ta, '<i>', '</i>')
                },
                {
                    id: 'underline-btn',
                    label: 'U̲',
                    title: 'Underline (<u>) (Ctrl+U)',
                    action: (ta) => applyFormat(ta, '<u>', '</u>')
                },
                {
                    id: 'strikethrough-btn',
                    label: 'S̶',
                    title: 'Strikethrough (<s>)',
                    action: (ta) => applyFormat(ta, '<s>', '</s>')
                },
                // Paragraph Group
                {
                    id: 'align-left-btn',
                    label: '⟦',
                    title: 'Align Left (<div align="left">)',
                    action: (ta) => applyFormat(ta, '<div align="left">', '</div>')
                },
                {
                    id: 'align-center-btn',
                    label: '⦳',
                    title: 'Align Center (<div align="center">)',
                    action: (ta) => applyFormat(ta, '<div align="center">', '</div>')
                },
                {
                    id: 'align-right-btn',
                    label: '⟧',
                    title: 'Align Right (<div align="right">)',
                    action: (ta) => applyFormat(ta, '<div align="right">', '</div>')
                },
                // Insert Group
                {
                    id: 'link-btn',
                    label: '🔗',
                    title: 'Insert Link (<a href="...">)',
                    action: (ta) => insertLink(ta)
                },
                {
                    id: 'tn-btn',
                    label: 'TN',
                    title: 'Translator Note (<tn>)',
                    action: (ta) => applyFormat(ta, '<tn>', '</tn>')
                },
                // Clear/History/Settings Group
                {
                    id: 'clear-btn',
                    label: 'Clear',
                    title: 'Clear Formatting (strip HTML tags)',
                    action: (ta) => clearFormatting(ta)
                },
                {
                    id: 'undo-btn',
                    label: '↶',
                    title: 'Undo (Ctrl+Z)',
                    action: (ta) => undo(ta)
                },
                {
                    id: 'redo-btn',
                    label: '↷',
                    title: 'Redo (Ctrl+Y)',
                    action: (ta) => redo(ta)
                },
                {
                    id: 'toggle-resize-btn',
                    label: 'AR',
                    title: 'Toggle Auto-Resize On/Off',
                    action: (ta) => toggleAutoResize()
                },
                {
                    id: 'settings-btn',
                    label: '⚙',
                    title: 'Set Min Width/Height for Auto-Resize',
                    action: (ta) => openSettings()
                }
            ],
            shortcuts: {
                bold: 'Ctrl+B',
                italic: 'Ctrl+I',
                underline: 'Ctrl+U',
                undo: 'Ctrl+Z',
                redo: 'Ctrl+Y'
            },
            manualTrigger: 'Ctrl+Shift+I', // Force inject
            historyLimit: 50, // Max undo/redo states
            commonColors: ['#FF0000', '#FF8000', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#8B00FF'], // ROYGBIV hex
            autoResize: {
                enabled: true,
                defaultWidth: 523,
                defaultHeight: 362
            },
            styles: `
                #${'note-formatting-toolbar'} {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 10px;
                    margin: 5px 0;
                    padding: 5px;
                    background: #f0f0f0;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    font-family: sans-serif;
                    font-size: 12px;
                    justify-content: center;
                    overflow-x: auto;
                    max-width: 100%;
                }
                #${'note-formatting-toolbar'} .group {
                    display: flex;
                    gap: 2px;
                    padding: 2px 5px;
                    background: white;
                    border-radius: 3px;
                    border: 1px solid #ddd;
                }
                #${'note-formatting-toolbar'} button {
                    padding: 4px 6px;
                    border: 1px solid #ccc;
                    background: white;
                    border-radius: 3px;
                    cursor: pointer;
                    font-weight: bold;
                    font-size: 11px;
                    min-width: auto;
                }
                #${'note-formatting-toolbar'} button:hover {
                    background: #e0e0e0;
                }
                .note-edit-dialog .ui-dialog-content {
                    position: relative;
                }
                .note-edit-dialog #note-formatting-toolbar {
                    margin-bottom: 5px;
                }
                #toolbar-minimize {
                    margin-left: auto;
                    background: #ddd;
                    font-size: 10px;
                    padding: 2px 4px;
                }
                #debug-indicator {
                    position: fixed;
                    top: 10px;
                    right: 10px;
                    background: #ffeb3b;
                    color: #000;
                    padding: 5px;
                    border: 1px solid #ccc;
                    font-size: 12px;
                    z-index: 9999;
                    display: none;
                    max-width: 300px;
                }
                /* Color Picker Styles */
                #color-picker {
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: #1E1E2C;
                    color: #E0E0E0;
                    border: 1px solid #ccc;
                    border-radius: 8px;
                    padding: 20px;
                    box-shadow: 0 4px 20px rgba(0,0,0,0.5);
                    z-index: 10000;
                    min-width: 300px;
                    font-family: sans-serif;
                }
                #color-picker h3 {
                    margin: 0 0 10px 0;
                    text-align: center;
                    color: #F0F0F0;
                }
                .palette {
                    display: grid;
                    grid-template-columns: repeat(7, 1fr);
                    gap: 5px;
                    margin-bottom: 15px;
                }
                .palette button {
                    width: 30px;
                    height: 30px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    cursor: pointer;
                }
                .custom-section {
                    margin-bottom: 10px;
                }
                .custom-section label {
                    display: block;
                    margin-bottom: 5px;
                    color: #D0D0D0;
                }
                .rgb-sliders {
                    display: flex;
                    gap: 10px;
                    margin-bottom: 10px;
                }
                .rgb-sliders input[type="range"] {
                    flex: 1;
                    background: #333;
                    color: #fff;
                }
                .rgb-inputs {
                    display: flex;
                    gap: 5px;
                }
                .rgb-inputs input[type="number"] {
                    width: 50px;
                    background: #333;
                    color: #fff;
                    border: 1px solid #666;
                }
                #brightness-slider {
                    margin-bottom: 10px;
                }
                #color-preview {
                    width: 100%;
                    height: 30px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    margin-bottom: 10px;
                }
                .picker-buttons {
                    display: flex;
                    gap: 10px;
                    justify-content: center;
                }
                .picker-buttons button {
                    padding: 8px 16px;
                    border: 1px solid #666;
                    border-radius: 4px;
                    cursor: pointer;
                    background: #333;
                    color: #E0E0E0;
                }
                .picker-buttons button:hover {
                    background: #444;
                }
                #color-picker-overlay {
                    position: fixed;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    background: rgba(0,0,0,0.5);
                    z-index: 9999;
                }
            `
        };
        // Global variables for state management (per-tab isolation for multi-tab support)
        let observer = null;
        let activeDialogs = new Set(); // Track multiple dialogs if possible (rare, but robust)
        let toolbar = null;
        let textarea = null;
        let isMinimized = false;
        let debugMode = true; // Enable for logging; set to false in prod if needed
        let manualTriggerHandler = null;
        let history = []; // Undo/redo history array of {value, start, end}
        let historyIndex = -1; // Current position in history
        let colorPicker = null; // Color picker dialog
        // Utility: Log with prefix for easy console filtering - uses CONFIG.version safely
        function log(message, data = null) {
            const version = CONFIG ? CONFIG.version : 'unknown';
            console.log(`[NoteFmtHelper v${version}] ${message}`, data || '');
        }
        // Load auto-resize config from localStorage (persistent across tabs/sessions)
        function loadAutoResizeConfig() {
            try {
                const savedEnabled = localStorage.getItem('nfh_autoResizeEnabled');
                if (savedEnabled !== null) {
                    CONFIG.autoResize.enabled = savedEnabled === 'true';
                }
                const savedWidth = localStorage.getItem('nfh_defaultWidth');
                if (savedWidth && !isNaN(parseInt(savedWidth))) {
                    CONFIG.autoResize.defaultWidth = parseInt(savedWidth);
                }
                const savedHeight = localStorage.getItem('nfh_defaultHeight');
                if (savedHeight && !isNaN(parseInt(savedHeight))) {
                    CONFIG.autoResize.defaultHeight = parseInt(savedHeight);
                }
                log('Auto-resize config loaded from localStorage.', CONFIG.autoResize);
            } catch (err) {
                log('Load auto-resize config error', err);
            }
        }
        // Utility: Inject styles if not already present - stringified to avoid const issues
        function injectStyles() {
            try {
                if (document.getElementById('note-formatting-styles')) return;
                const style = document.createElement('style');
                style.id = 'note-formatting-styles';
                style.textContent = CONFIG.styles; // Template interpolation already handles ID replacement
                document.head.appendChild(style);
                log('Styles injected.');
            } catch (err) {
                console.error('[NoteFmtHelper] Style injection error:', err);
            }
        }
        // Utility: Create or update toolbar with minimize button (Word-like ribbon with groups)
        function createToolbar() {
            try {
                if (toolbar) return toolbar;
                toolbar = document.createElement('div');
                toolbar.id = CONFIG.toolbarId;
                // Groups for Word-like layout
                const groups = {
                    font: ['font-size-btn', 'big-btn', 'small-btn', 'color-btn', 'highlight-btn', 'bold-btn', 'italic-btn', 'underline-btn', 'strikethrough-btn'],
                    paragraph: ['align-left-btn', 'align-center-btn', 'align-right-btn'],
                    insert: ['link-btn', 'tn-btn'],
                    history: ['clear-btn', 'undo-btn', 'redo-btn', 'toggle-resize-btn', 'settings-btn']
                };
                Object.entries(groups).forEach(([groupName, btnIds]) => {
                    const group = document.createElement('div');
                    group.className = 'group';
                    group.title = groupName.charAt(0).toUpperCase() + groupName.slice(1); // e.g., "Font"
                    btnIds.forEach(btnId => {
                        const btnConfig = CONFIG.buttons.find(b => b.id === btnId);
                        if (btnConfig) {
                            const button = document.createElement('button');
                            button.id = btnConfig.id;
                            button.type = 'button';
                            button.innerHTML = btnConfig.label;
                            button.title = btnConfig.title;
                            button.addEventListener('click', (e) => {
                                e.preventDefault();
                                e.stopPropagation();
                                if (textarea && !textarea.disabled) {
                                    if (document.activeElement !== textarea) textarea.focus();
                                    // Pre-snapshot for formatting actions (exclude undo/redo/toggle/settings)
                                    if (!['undo-btn', 'redo-btn', 'toggle-resize-btn', 'settings-btn'].includes(btnConfig.id)) {
                                        saveState(textarea);
                                    }
                                    btnConfig.action(textarea);
                                    log(`Button ${btnConfig.id} clicked.`);
                                } else if (['toggle-resize-btn', 'settings-btn'].includes(btnConfig.id)) {
                                    // Allow these even without textarea focus (global config)
                                    btnConfig.action();
                                    log(`${btnConfig.id} button clicked (global).`);
                                } else {
                                    log('Button clicked but textarea not ready.', { focused: document.activeElement });
                                }
                            });
                            group.appendChild(button);
                        }
                    });
                    toolbar.appendChild(group);
                });
                // Minimize button (smaller, optional for buttonset fit)
                const minimizeBtn = document.createElement('button');
                minimizeBtn.id = 'toolbar-minimize';
                minimizeBtn.type = 'button';
                minimizeBtn.innerHTML = '−'; // Unicode minus for minimize
                minimizeBtn.title = 'Minimize Formatting Toolbar';
                minimizeBtn.style.fontSize = '0.8em';
                minimizeBtn.style.padding = '0.3em 0.5em';
                minimizeBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    toggleMinimize();
                });
                toolbar.appendChild(minimizeBtn);
                // Version display (small span, not a button) - in corner as per style
                const versionSpan = document.createElement('span');
                versionSpan.id = 'toolbar-version';
                versionSpan.textContent = `v${CONFIG.version}`;
                versionSpan.className = 'ui-widget';
                versionSpan.style.cssText = 'margin-left: 5px; font-size: 0.8em; color: #666; padding: 0.4em 0; align-self: center;';
                toolbar.appendChild(versionSpan);
                // Responsive: Adjust on resize
                window.addEventListener('resize', adjustToolbarLayout);
                adjustToolbarLayout();
                log('Toolbar created as Word-like ribbon with groups and icons.');
                return toolbar;
            } catch (err) {
                log('Toolbar creation error', err);
                return null;
            }
        }
        // Minimize/Maximize functionality for better UX (hides main buttons)
        function toggleMinimize() {
            try {
                isMinimized = !isMinimized;
                const mainButtons = toolbar ? toolbar.querySelectorAll('button:not(#toolbar-minimize)') : [];
                const versionSpan = document.getElementById('toolbar-version');
                const minimizeBtn = document.getElementById('toolbar-minimize');
                if (isMinimized) {
                    mainButtons.forEach(btn => btn.style.display = 'none');
                    if (versionSpan) versionSpan.style.display = 'none';
                    if (minimizeBtn) {
                        minimizeBtn.innerHTML = '+'; // Plus for maximize
                        minimizeBtn.title = 'Expand Formatting Toolbar';
                    }
                    log('Formatting toolbar minimized.');
                } else {
                    mainButtons.forEach(btn => btn.style.display = '');
                    if (versionSpan) versionSpan.style.display = '';
                    if (minimizeBtn) {
                        minimizeBtn.innerHTML = '−';
                        minimizeBtn.title = 'Minimize Formatting Toolbar';
                    }
                    adjustToolbarLayout();
                    log('Formatting toolbar expanded.');
                }
            } catch (err) {
                log('Toggle minimize error', err);
            }
        }
        // Responsive layout adjustment (for buttonset fit)
        function adjustToolbarLayout() {
            try {
                if (toolbar && window.innerWidth < 800) { // Broader check for dialog context
                    toolbar.style.flexDirection = 'column';
                    toolbar.style.alignItems = 'stretch';
                    toolbar.querySelectorAll('button').forEach(btn => btn.style.minWidth = 'auto');
                } else {
                    toolbar.style.flexDirection = 'row';
                    toolbar.style.alignItems = 'center';
                    toolbar.querySelectorAll('button').forEach(btn => btn.style.minWidth = 'initial');
                }
            } catch (err) {
                log('Layout adjustment error', err);
            }
        }
        // Auto-expand dialog to fit ribbon + textarea
        function autoResizeDialog(dialog) {
            try {
                if (!CONFIG.autoResize.enabled) {
                    log('Auto-resize disabled; skipped.');
                    return;
                }
                if (dialog.dataset.helperResized === 'true') return; // One-time per dialog (reset via settings if needed)
                const tb = dialog.querySelector(`#${CONFIG.toolbarId}`);
                const ta = dialog.querySelector('textarea');
                if (!tb || !ta) {
                    log('Auto-resize skipped: Missing toolbar/textarea.');
                    return;
                }
                // Re-measure after height set (no inner timeout needed for immediacy)
                const tbRect = tb.getBoundingClientRect();
                const taRect = ta.getBoundingClientRect();
                // Calculate content dimensions
                const contentWidth = Math.max(tbRect.width, taRect.width) + 30; // Padding for borders/scroll
                const contentHeight = tbRect.height + taRect.height + 50; // Buffer for padding/margins
                const newWidth = Math.max(CONFIG.autoResize.defaultWidth, Math.max(500, contentWidth));
                const newHeight = Math.max(CONFIG.autoResize.defaultHeight, contentHeight + 150); // Header/footer/dialog chrome buffer
                // Always use direct style resize (reliable, no jQuery dependency/error)
                dialog.style.width = `${newWidth}px`;
                dialog.style.height = `${newHeight}px`;
                log(`Auto-resized dialog via direct style: ${newWidth}x${newHeight}px`);
                dialog.dataset.helperResized = 'true';
            } catch (err) {
                log('Auto-resize error', err);
            }
        }
        // Toggle auto-resize enable/disable
        function toggleAutoResize() {
            try {
                CONFIG.autoResize.enabled = !CONFIG.autoResize.enabled;
                localStorage.setItem('nfh_autoResizeEnabled', CONFIG.autoResize.enabled.toString());
                const status = CONFIG.autoResize.enabled ? 'enabled' : 'disabled';
                log(`Auto-resize ${status}.`);
                showDebugIndicator(`Auto-resize ${status}.`, 'info');
                // If enabling, re-apply to current dialog
                if (CONFIG.autoResize.enabled) {
                    const dialog = document.querySelector('.note-edit-dialog');
                    if (dialog) {
                        dialog.dataset.helperResized = 'false';
                        setTimeout(() => autoResizeDialog(dialog), 100);
                    }
                }
            } catch (err) {
                log('Toggle auto-resize error', err);
            }
        }
        // Open settings for width/height mins only
        function openSettings() {
            try {
                let newWidthStr = prompt('Enter default min width (px):', CONFIG.autoResize.defaultWidth.toString());
                let newWidth = parseInt(newWidthStr);
                if (isNaN(newWidth) || newWidth < 300) {
                    newWidth = CONFIG.autoResize.defaultWidth;
                    log('Invalid width; using default.');
                }
                CONFIG.autoResize.defaultWidth = newWidth;
                localStorage.setItem('nfh_defaultWidth', newWidth.toString());
                let newHeightStr = prompt('Enter default min height (px):', CONFIG.autoResize.defaultHeight.toString());
                let newHeight = parseInt(newHeightStr);
                if (isNaN(newHeight) || newHeight < 200) {
                    newHeight = CONFIG.autoResize.defaultHeight;
                    log('Invalid height; using default.');
                }
                CONFIG.autoResize.defaultHeight = newHeight;
                localStorage.setItem('nfh_defaultHeight', newHeight.toString());
                log('Auto-resize dimensions updated.', CONFIG.autoResize);
                showDebugIndicator(`Dimensions updated: ${newWidth}x${newHeight}`, 'info');
                // Re-apply to current dialog if enabled
                if (CONFIG.autoResize.enabled) {
                    const dialog = document.querySelector('.note-edit-dialog');
                    if (dialog) {
                        dialog.dataset.helperResized = 'false';
                        setTimeout(() => autoResizeDialog(dialog), 100);
                    }
                }
            } catch (err) {
                log('Settings error', err);
                alert('Settings update failed; check console.');
            }
        }
        // Unified apply format function for tags, styles, aligns (replaces applyStyle/applyAlign)
        function applyFormat(ta, openTag, closeTag) {
            try {
                const start = ta.selectionStart;
                const end = ta.selectionEnd;
                const text = ta.value;
                const selected = text.substring(start, end);
                const before = text.substring(0, start);
                const after = text.substring(end);
                let newText;
                let cursorStart, cursorEnd;
                if (selected) {
                    newText = before + openTag + selected + closeTag + after;
                    cursorStart = start + openTag.length;
                    cursorEnd = cursorStart + selected.length;
                } else {
                    const placeholder = openTag + 'text' + closeTag;
                    newText = before + placeholder + after;
                    cursorStart = start + openTag.length;
                    cursorEnd = cursorStart + 4; // Position inside 'text'
                }
                ta.value = newText;
                ta.selectionStart = cursorStart;
                ta.selectionEnd = cursorEnd;
                ta.focus();
                ta.dispatchEvent(new Event('input', { bubbles: true }));
                // Post-snapshot after successful apply
                saveState(ta);
                log(`Applied format: ${openTag.substring(0, 20)}...`);
            } catch (err) {
                log('Apply format error', err);
            }
        }
        // Prompt for font size and apply via applyFormat
        function promptFontSize(ta) {
            try {
                const size = prompt('Enter font size (e.g., 18px, 1.5em, large):', '16px');
                if (!size) return; // No post-save if cancelled
                const styleStr = `font-size: ${size};`;
                const openTag = `<span style="${styleStr}">`;
                applyFormat(ta, openTag, '</span>'); // Post-save inside applyFormat
            } catch (err) {
                log('Font size prompt error', err);
            }
        }
        // Open color picker for text color or highlight
        function openColorPicker(ta, type) {
            try {
                if (colorPicker) {
                    closeColorPicker();
                }
                // Overlay
                const overlay = document.createElement('div');
                overlay.id = 'color-picker-overlay';
                overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9998;';
                document.body.appendChild(overlay);
                // Picker dialog
                colorPicker = document.createElement('div');
                colorPicker.id = 'color-picker';
                colorPicker.innerHTML = `
                    <h3>${type === 'background-color' ? 'Text Highlight' : 'Text Color'}</h3>
                    <div class="palette"></div>
                    <div class="custom-section">
                        <label>Custom RGB</label>
                        <div class="rgb-sliders">
                            <input type="range" id="r-slider" min="0" max="255" value="255" />
                            <input type="range" id="g-slider" min="0" max="255" value="0" />
                            <input type="range" id="b-slider" min="0" max="255" value="0" />
                        </div>
                        <div class="rgb-inputs">
                            <span>R:</span><input type="number" id="r-input" min="0" max="255" value="255" />
                            <span>G:</span><input type="number" id="g-input" min="0" max="255" value="0" />
                            <span>B:</span><input type="number" id="b-input" min="0" max="255" value="0" />
                        </div>
                        <label>Brightness (Alpha 0-1)</label>
                        <input type="range" id="alpha-slider" min="0" max="1" step="0.01" value="1" />
                        <span id="alpha-value">1.00</span>
                    </div>
                    <div id="color-preview" style="background-color: rgb(255,0,0);"></div>
                    <div class="picker-buttons">
                        <button id="picker-ok">OK</button>
                        <button id="picker-cancel">Cancel</button>
                    </div>
                `;
                document.body.appendChild(colorPicker);
                // Palette
                const palette = colorPicker.querySelector('.palette');
                CONFIG.commonColors.forEach(color => {
                    const btn = document.createElement('button');
                    btn.style.backgroundColor = color;
                    btn.addEventListener('click', () => {
                        const rgb = hexToRgb(color);
                        colorPicker.querySelector('#r-slider').value = rgb.r;
                        colorPicker.querySelector('#g-slider').value = rgb.g;
                        colorPicker.querySelector('#b-slider').value = rgb.b;
                        colorPicker.querySelector('#r-input').value = rgb.r;
                        colorPicker.querySelector('#g-input').value = rgb.g;
                        colorPicker.querySelector('#b-input').value = rgb.b;
                        updateColor(colorPicker);
                    });
                    palette.appendChild(btn);
                });
                // OK button - post-save after apply
                colorPicker.querySelector('#picker-ok').addEventListener('click', (e) => {
                    const r = colorPicker.querySelector('#r-slider').value;
                    const g = colorPicker.querySelector('#g-slider').value;
                    const b = colorPicker.querySelector('#b-slider').value;
                    const alpha = colorPicker.querySelector('#alpha-slider').value;
                    const colorValue = alpha < 1 ? `rgba(${r}, ${g}, ${b}, ${alpha})` : `rgb(${r}, ${g}, ${b})`;
                    const styleStr = `${type}: ${colorValue};`;
                    const openTag = `<span style="${styleStr}">`;
                    applyFormat(ta, openTag, '</span>'); // Post-save inside applyFormat
                    closeColorPicker();
                });
                // Cancel button
                colorPicker.querySelector('#picker-cancel').addEventListener('click', closeColorPicker);
                // Close on overlay click
                overlay.addEventListener('click', closeColorPicker);
                // ESC key to close
                const escHandler = (e) => {
                    if (e.key === 'Escape') closeColorPicker();
                };
                window.addEventListener('keydown', escHandler);
                // Update color preview
                function updateColor(picker) {
                    const r = picker.querySelector('#r-slider').value;
                    const g = picker.querySelector('#g-slider').value;
                    const b = picker.querySelector('#b-slider').value;
                    const alpha = picker.querySelector('#alpha-slider').value;
                    picker.querySelector('#alpha-value').textContent = alpha;
                    const color = `rgba(${r}, ${g}, ${b}, ${alpha})`;
                    picker.querySelector('#color-preview').style.backgroundColor = color;
                    picker.querySelector('#r-input').value = r;
                    picker.querySelector('#g-input').value = g;
                    picker.querySelector('#b-input').value = b;
                }
                // Update slider from input
                function updateSlider(channel, picker) {
                    const input = picker.querySelector(`#${channel}-input`).value;
                    picker.querySelector(`#${channel}-slider`).value = input;
                    updateColor(picker);
                }
                // Add event listeners for inputs/sliders
                colorPicker.addEventListener('input', (e) => {
                    if (e.target.matches('input[type="range"], input[type="number"]')) {
                        if (e.target.id.includes('input') && e.target.type === 'number') {
                            const channel = e.target.id.split('-')[0];
                            updateSlider(channel, colorPicker);
                        } else {
                            updateColor(colorPicker);
                        }
                    }
                });
                // Initial update
                updateColor(colorPicker);
                // Cleanup ESC on close
                function closeColorPicker() {
                    if (colorPicker) colorPicker.remove();
                    if (overlay) overlay.remove();
                    colorPicker = null;
                    window.removeEventListener('keydown', escHandler);
                }
                function hexToRgb(hex) {
                    const r = parseInt(hex.substr(1,2), 16);
                    const g = parseInt(hex.substr(3,2), 16);
                    const b = parseInt(hex.substr(5,2), 16);
                    return { r, g, b };
                }
            } catch (err) {
                log('Color picker open error', err);
            }
        }
        // Clear formatting: Strip HTML tags
        function clearFormatting(ta) {
            try {
                const text = ta.value.replace(/<[^>]*>/g, '');
                ta.value = text;
                ta.selectionStart = ta.selectionEnd = 0;
                ta.focus();
                ta.dispatchEvent(new Event('input', { bubbles: true }));
                // Post-snapshot after clear
                saveState(ta);
                log('Formatting cleared.');
            } catch (err) {
                log('Clear formatting error', err);
            }
        }
        // Insert link prompt with intelligent defaults
        function insertLink(ta) {
            try {
                const start = ta.selectionStart;
                const end = ta.selectionEnd;
                const selected = ta.value.substring(start, end);
                const url = prompt('Enter URL:');
                if (!url) return; // No post-save if cancelled
                let text;
                if (selected) {
                    text = prompt('Enter link text (optional):', selected) || selected;
                } else {
                    text = prompt('Enter link text (optional):', '') || url;
                }
                const linkHTML = `<a href="${url}">${text}</a>`;
                insertAtCursor(ta, linkHTML); // Post-save inside insertAtCursor
            } catch (err) {
                log('Link insert error', err);
            }
        }
        // Insert text at cursor
        function insertAtCursor(ta, text) {
            try {
                const start = ta.selectionStart;
                const end = ta.selectionEnd;
                ta.value = ta.value.substring(0, start) + text + ta.value.substring(end);
                ta.selectionStart = ta.selectionEnd = start + text.length;
                ta.focus();
                ta.dispatchEvent(new Event('input', { bubbles: true }));
                // Post-snapshot after insert
                saveState(ta);
            } catch (err) {
                log('Insert at cursor error', err);
            }
        }
        // History management for undo/redo - now saves {value, start, end}
        function saveState(ta) {
            try {
                const state = {
                    value: ta.value,
                    start: ta.selectionStart,
                    end: ta.selectionEnd
                };
                // Trim history to limit
                if (history.length > CONFIG.historyLimit) {
                    history.shift();
                    if (historyIndex > 0) historyIndex--;
                }
                // Push current state after current index (linear, discards branches)
                history = history.slice(0, historyIndex + 1);
                history.push(state);
                historyIndex = history.length - 1;
                log(`State saved. History length: ${history.length}`);
            } catch (err) {
                log('Save state error', err);
            }
        }
        function undo(ta) {
            try {
                if (historyIndex > 0) {
                    historyIndex--;
                    const state = history[historyIndex];
                    ta.value = state.value;
                    ta.selectionStart = state.start;
                    ta.selectionEnd = state.end;
                    ta.focus();
                    ta.dispatchEvent(new Event('input', { bubbles: true }));
                    log(`Undo to index ${historyIndex}`);
                } else {
                    log('Undo: At start of history.');
                }
            } catch (err) {
                log('Undo error', err);
            }
        }
        function redo(ta) {
            try {
                if (historyIndex < history.length - 1) {
                    historyIndex++;
                    const state = history[historyIndex];
                    ta.value = state.value;
                    ta.selectionStart = state.start;
                    ta.selectionEnd = state.end;
                    ta.focus();
                    ta.dispatchEvent(new Event('input', { bubbles: true }));
                    log(`Redo to index ${historyIndex}`);
                } else {
                    log('Redo: At end of history.');
                }
            } catch (err) {
                log('Redo error', err);
            }
        }
        // Setup initial history snapshot (no ongoing listener)
        function setupHistory(ta) {
            try {
                // Initial save (pre-any-changes backup)
                saveState(ta);
                log('Initial history snapshot set up.');
            } catch (err) {
                log('History setup error', err);
            }
        }
        // Keyboard shortcuts (per-tab, checks focus) - extended for undo/redo
        function setupShortcuts() {
            try {
                // Remove existing listener to avoid duplicates in multi-tab/reload
                document.removeEventListener('keydown', handleKeydown);
                document.addEventListener('keydown', handleKeydown, true);
                log('Shortcuts set up.');
            } catch (err) {
                log('Shortcuts setup error', err);
            }
        }
        function handleKeydown(e) {
            try {
                if (!textarea || document.activeElement !== textarea) return;
                if (e.ctrlKey) {
                    if (e.key === 'b') {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'bold-btn').action(textarea);
                    } else if (e.key === 'i') {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'italic-btn').action(textarea);
                    } else if (e.key === 'u') {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'underline-btn').action(textarea);
                    } else if (e.key === 'z' && !e.shiftKey) {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'undo-btn').action(textarea);
                    } else if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) {
                        e.preventDefault();
                        CONFIG.buttons.find(b => b.id === 'redo-btn').action(textarea);
                    }
                }
            } catch (err) {
                log('Keydown handler error', err);
            }
        }
        // Manual force inject handler - defined early to avoid ReferenceError
        function setupManualTrigger() {
            try {
                if (manualTriggerHandler) return;
                manualTriggerHandler = (e) => {
                    if (e.ctrlKey && e.shiftKey && e.key === 'I') {
                        e.preventDefault();
                        const dialog = document.querySelector('.note-edit-dialog');
                        if (dialog) {
                            injectToolbar(dialog, true); // Force with banner
                            log('Manual trigger: Forced injection.');
                        } else {
                            showDebugIndicator('Manual trigger: No dialog open.', 'error');
                        }
                    }
                };
                document.addEventListener('keydown', manualTriggerHandler, true);
                log('Manual trigger (Ctrl+Shift+I) set up.');
            } catch (err) {
                log('Manual trigger setup error', err);
            }
        }
        // Dialog injection: Insert ribbon above textarea
        function injectToolbar(dialog, isManual = false) {
            try {
                log('Attempting injection for dialog:', dialog);
                const content = dialog.querySelector('.ui-dialog-content');
                if (!content) {
                    log('No .ui-dialog-content found.');
                    return;
                }
                const headerSpan = content.querySelector('span');
                if (!headerSpan) {
                    log('No header span found.');
                    return;
                }
                textarea = content.querySelector('textarea');
                if (!textarea) {
                    log('No textarea found in content.');
                    return;
                }
                // Stable injection check: Use dataset attribute to prevent re-inject
                if (dialog.dataset.helperInjected === 'true') {
                    log('Dialog already injected (dataset check). Skipping.');
                    return;
                }
                // Remove any existing toolbar in content
                const existing = content.querySelector(`#${CONFIG.toolbarId}`);
                if (existing) {
                    existing.remove();
                    log('Removed existing toolbar.');
                }
                const tb = createToolbar();
                if (tb) {
                    content.insertBefore(tb, textarea); // Insert after header, before textarea
                    log('Formatting ribbon inserted above textarea.');
                } else {
                    log('Failed to create toolbar.');
                    return;
                }
                // Early textarea height adjustment for immediate fit
                const taFullHeight = Math.max(200, textarea.scrollHeight);
                textarea.style.height = `${taFullHeight}px`;
                // Mark as injected
                dialog.dataset.helperInjected = 'true';
                setupShortcuts(); // Re-setup for this textarea
                setupHistory(textarea); // Initial snapshot only
                // Auto-resize dialog immediately after insertion (minimal delay for layout settle)
                setTimeout(() => autoResizeDialog(dialog), 100);
                // Cleanup on dialog close - remove dataset
                const observerCleanup = new MutationObserver((mutations) => {
                    if (!document.body.contains(dialog)) {
                        activeDialogs.delete(dialog); // Track by reference now
                        if (tb && tb.parentNode) tb.remove();
                        delete dialog.dataset.helperInjected;
                        delete dialog.dataset.helperResized;
                        history = [];
                        historyIndex = -1;
                        if (colorPicker) closeColorPicker();
                        observerCleanup.disconnect();
                        log(`Cleaned up for closed dialog.`);
                    }
                });
                observerCleanup.observe(document.body, { childList: true, subtree: true });
                // Only show banner on first auto or manual
                if (isManual || !activeDialogs.has(dialog)) {
                    log(`Ribbon injected above textarea for dialog.`);
                    showDebugIndicator('Word-like Ribbon Injected Above Textarea! Icons and Big/Small added.', 'success');
                }
                activeDialogs.add(dialog); // Track by reference for multi-dialog
            } catch (err) {
                log('Injection error', err);
                showDebugIndicator('Injection Failed - Check Console. Try Ctrl+Shift+I.', 'error');
                // Autonomous recovery: Poll will retry
            }
        }
        // Debug indicator: Temporary overlay for visibility issues
        function showDebugIndicator(message, type = 'info') {
            try {
                if (!debugMode) return;
                let indicator = document.getElementById('debug-indicator');
                if (!indicator) {
                    indicator = document.createElement('div');
                    indicator.id = 'debug-indicator';
                    document.body.appendChild(indicator);
                }
                indicator.textContent = `[NoteFmtHelper] ${message}`;
                indicator.style.background = type === 'error' ? '#ffcccc' : type === 'success' ? '#ccffcc' : '#ffeb3b';
                indicator.style.display = 'block';
                setTimeout(() => { indicator.style.display = 'none'; }, 5000); // Shorter now to reduce spam
                log(message);
            } catch (err) {
                console.error('[NoteFmtHelper] Debug indicator error', err);
            }
        }
        // Main observer for dialog detection (subtree for nested changes)
        function setupObserver() {
            try {
                if (observer) {
                    log('Observer already set up.');
                    return;
                }
                observer = new MutationObserver((mutations) => {
                    let detected = false;
                    mutations.forEach((mutation) => {
                        if (mutation.type === 'childList') {
                            mutation.addedNodes.forEach((node) => {
                                if (node.nodeType === 1 && node.matches && node.matches('.note-edit-dialog')) {
                                    log('Dialog added via observer:', node);
                                    detected = true;
                                    setTimeout(() => injectToolbar(node), 500); // Keep for render stability
                                }
                            });
                        }
                    });
                    if (detected) log('Observer triggered injection.');
                });
                observer.observe(document.body, { childList: true, subtree: true });
                log('Observer set up.');
            } catch (err) {
                log('Observer setup error', err);
            }
        }
        // Fallback poll for edge cases (e.g., observer misses, multi-tab glitches) - faster for persistence
        let pollInterval = null;
        function startPoll() {
            try {
                if (pollInterval) return;
                pollInterval = setInterval(() => {
                    const dialogs = document.querySelectorAll('.note-edit-dialog');
                    if (dialogs.length === 0) {
                        if (toolbar) {
                            toolbar.remove();
                            toolbar = null;
                            activeDialogs.clear();
                            log('No dialogs; cleaned up.');
                        }
                        return;
                    }
                    dialogs.forEach(dialog => {
                        // Use dataset check here too
                        if (dialog.dataset.helperInjected !== 'true') {
                            const content = dialog.querySelector('.ui-dialog-content');
                            const ta = content ? content.querySelector('textarea') : null;
                            if (content && ta) {
                                log('Poll detected eligible dialog:', dialog);
                                injectToolbar(dialog);
                            }
                        }
                    });
                }, 3000); // Slower poll (3s) now that dataset prevents retry
                log('Poll started (every 3s).');
            } catch (err) {
                log('Poll start error', err);
            }
        }
        // Init: Robust, multi-tab safe - setupManualTrigger called after definition
        function init() {
            try {
                loadAutoResizeConfig(); // Load user settings first
                injectStyles();
                setupObserver();
                startPoll(); // Always run poll for reliability
                setupManualTrigger(); // Defined early, no ReferenceError
                // Multi-tab: Listen for focus to re-inject if needed
                window.addEventListener('focus', (e) => {
                    setTimeout(() => {
                        setupObserver();
                        if (pollInterval) clearInterval(pollInterval);
                        startPoll();
                    }, 100);
                });
                showDebugIndicator(`Script Loaded v${CONFIG.version}! Open dialog for Word-like ribbon & advanced markup. Ctrl+Shift+I to force. AR/⚙ for auto-resize.`, 'info');
                log(`Note Formatting Helper v${CONFIG.version} initialized. Ready for multi-tab use. Open DevTools > Console to monitor logs.`);
            } catch (err) {
                console.error('[NoteFmtHelper] Init error:', err);
                showDebugIndicator('Init error - script partially loaded. Try Ctrl+Shift+I on dialog open.', 'error');
            }
        }
        // DOM ready check
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }
        // Handle page navigation (Danbooru hash changes)
        window.addEventListener('hashchange', (e) => {
            setTimeout(() => {
                setupObserver();
                if (pollInterval) clearInterval(pollInterval);
                startPoll();
            }, 500);
            log('Page navigated; re-setup observers.');
        });
    } catch (globalErr) {
        console.error('[NoteFmtHelper] Global error - script failed to load:', globalErr);
        // Fallback: Create minimal debug indicator
        const indicator = document.createElement('div');
        indicator.id = 'debug-indicator';
        indicator.style.cssText = 'position:fixed;top:10px;right:10px;background:#ffcccc;color:#000;padding:5px;border:1px solid #ccc;z-index:9999;';
        indicator.textContent = '[NoteFmtHelper] Fatal error - reinstall script. Check console.';
        document.body.appendChild(indicator);
    }
})();