HTML Content to Markdown

Convert selected HTML Content to Markdown with filtering

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         HTML Content to Markdown
// @name:zh      网页内容转Markdown
// @namespace    https://github.com/ChuwuYo
// @homepageURL  https://github.com/ChuwuYo/misc-files/blob/main/userscripts/HTML%20Content%20to%20Markdown.user.js
// @supportURL   https://github.com/ChuwuYo/misc-files/issues
// @version      0.1.3
// @description  Convert selected HTML Content to Markdown with filtering
// @description:zh 将选定的HTML内容转换为Markdown(规则过滤)
// @author       ChuwuYo
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_getValue
// @icon         https://litera-reader.com/favicon.png?v=2
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://unpkg.com/turndown/dist/turndown.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
// @require      https://unpkg.com/@guyplusplus/turndown-plugin-gfm/dist/turndown-plugin-gfm.js
// @license      AGPL-3.0
// @TODO         1.消息国际化 zh-cn/en-us
// ==/UserScript==

(function () {
    'use strict';

    // --- User Config Defaults ---
    const DEFAULT_SHORTCUT_CONFIG = {
        "Shift": false,
        "Ctrl": true,
        "Alt": false,
        "Key": "m"
    };
    const DEFAULT_FILTER_CONFIG = {
        removeTags: ['script', 'style', 'link', 'meta', 'iframe', 'noscript', 'object', 'embed', 'button', 'input', 'textarea', 'select', 'option', 'form', 'video', 'audio', 'canvas', 'map', 'area', 'track', 'applet', 'bgsound', 'blink', 'isindex', 'keygen', 'marquee', 'menuitem', 'nextid', 'noembed', 'param', 'source'],
        removeAttributes: [
            'style', 'onclick', 'onload', 'onerror', 'onmouseover', 'onmouseout',
            'onfocus', 'onblur', 'target', 'contenteditable', 'draggable',
            'tabindex', 'spellcheck', 'translate', 'dir', 'lang',
            'aria-\\w+', 'data-\\w+'
        ],
        keepAttributesOnTags: {
            'img': ['src', 'alt', 'title', 'width', 'height'],
            'a': ['href', 'title', 'rel'],
            'code': ['class'],
            'pre': ['class'],
            'table': ['class'],
            'th': ['scope', 'colspan', 'rowspan'],
            'td': ['colspan', 'rowspan']
        },
        removeElementsWithClasses: ['advertisement', 'ads', 'sidebar', 'footer', 'header', 'nav', 'menu'],
        removeElementsWithIds: ['advertisement', 'ads', 'sidebar', 'footer', 'header', 'nav', 'menu'],
        smartContentDetection: true,
        preserveCodeBlocks: true
    };

    // --- User-Provided Config (can be empty) ---
    const shortCutUserConfig = { /* Example: "Key": "s" */ };
    const filterUserConfig = { /* Example: removeTags: ['script', 'style', 'header'] */ };

    // --- Global Variables ---
    let isSelecting = false;
    let isMultiSelectMode = false;
    let hoveredElement = null;
    let selectedElements = [];
    let shortCutConfig;
    let filterConfig;

    const closeButtonSvgIcon = '<svg viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1 1 13.658 2.343 8 8 0 0 1 2.343 13.657M6.03 4.97a.75.75 0 0 0-1.042.018.75.75 0 0 0-.018 1.042L6.94 8 4.97 9.97a.749.749 0 0 0 .326 1.275.75.75 0 0 0 .734-.215L8 9.06l1.97 1.97a.749.749 0 0 0 1.275-.326.75.75 0 0 0-.215-.734L9.06 8l1.97-1.97a.749.749 0 0 0-.326-1.275.75.75 0 0 0-.734.215L8 6.94Z"/></svg>';

    // --- Helper Functions ---
    function loadConfig(storageKey, defaultConfig, userProvidedConfig) {
        let mergedConfig = { ...defaultConfig };
        const storedConfigStr = GM_getValue(storageKey);

        if (storedConfigStr) {
            try {
                const storedConfig = storedConfigStr ? JSON.parse(storedConfigStr) : {};
                mergedConfig = { ...defaultConfig, ...storedConfig };
            } catch (e) {
                console.error(`[HTML to MD] Error parsing stored config for ${storageKey}:`, e, "\nStored string was:", storedConfigStr);
                GM_setValue(storageKey, JSON.stringify(defaultConfig)); // Reset to default if parsing fails
                mergedConfig = { ...defaultConfig };
            }
        } else {
            GM_setValue(storageKey, JSON.stringify(defaultConfig));
        }

        if (userProvidedConfig && Object.keys(userProvidedConfig).length > 0) {
            mergedConfig = { ...mergedConfig, ...userProvidedConfig };
            GM_setValue(storageKey, JSON.stringify(mergedConfig));
        }
        return mergedConfig;
    }

    // --- Initialize Configurations ---
    try {
        shortCutConfig = loadConfig('shortCutConfig', DEFAULT_SHORTCUT_CONFIG, shortCutUserConfig);
        filterConfig = loadConfig('filterConfig', DEFAULT_FILTER_CONFIG, filterUserConfig);
    } catch (e) {
        console.error("[HTML to MD] Critical error loading configuration:", e);
        shortCutConfig = { ...DEFAULT_SHORTCUT_CONFIG }; // Fallback to defaults
        filterConfig = { ...DEFAULT_FILTER_CONFIG };   // Fallback to defaults
        alert("Error loading script configuration. Using default settings. Please check console for details.");
    }

    // --- Turndown Service Setup ---
    const turndownService = new TurndownService({
        codeBlockStyle: 'fenced', headingStyle: 'atx', hr: '---',
        bulletListMarker: '-', emDelimiter: '*', strongDelimiter: '**',
        linkStyle: 'inlined', linkReferenceStyle: 'full'
    });
    TurndownPluginGfmService.gfm(turndownService);

    if (filterConfig && filterConfig.removeTags && Array.isArray(filterConfig.removeTags)) {
        turndownService.remove(filterConfig.removeTags);
    }
    turndownService.remove((node) => node.nodeType === Node.COMMENT_NODE);

    // Enhanced image handling
    turndownService.addRule('enhancedImages', {
        filter: 'img',
        replacement: function (content, node) {
            const alt = node.getAttribute('alt') || '';
            const src = node.getAttribute('src') || '';
            const title = node.getAttribute('title');
            if (!src) return alt;
            return title ? `![${alt}](${src} "${title}")` : `![${alt}](${src})`;
        }
    });

    // Enhanced link handling
    turndownService.addRule('enhancedLinks', {
        filter: function (node) {
            return node.nodeName === 'A' && node.getAttribute('href');
        },
        replacement: function (content, node) {
            const href = node.getAttribute('href');
            const title = node.getAttribute('title');
            if (!href || href.startsWith('javascript:') || href === '#') return content;
            return title ? `[${content}](${href} "${title}")` : `[${content}](${href})`;
        }
    });

    // --- Core Functions ---
    function convertToMarkdown(element) {
        if (!element) return '';
        const clonedElement = element.cloneNode(true);

        if (filterConfig) {
            clonedElement.querySelectorAll('*').forEach(el => {
                const tagName = el.tagName.toLowerCase();
                const attributesToKeep = (filterConfig.keepAttributesOnTags && filterConfig.keepAttributesOnTags[tagName]) || [];

                if (filterConfig.removeAttributes && Array.isArray(filterConfig.removeAttributes)) {
                    Array.from(el.attributes).forEach(attr => {
                        const attrName = attr.name.toLowerCase();
                        if (attributesToKeep.includes(attrName)) {
                            return;
                        }
                        let shouldRemove = false;
                        for (const pattern of filterConfig.removeAttributes) {
                            if (pattern.includes('\\w+')) {
                                const regex = new RegExp('^' + pattern.replace('\\w+', '\\w+') + '$', 'i');
                                if (regex.test(attrName)) {
                                    shouldRemove = true;
                                    break;
                                }
                            } else if (attrName === pattern.toLowerCase()) {
                                shouldRemove = true;
                                break;
                            }
                        }
                        if (shouldRemove) {
                            el.removeAttribute(attr.name);
                        }
                    });
                }
            });

            if (filterConfig.removeElementsWithClasses && Array.isArray(filterConfig.removeElementsWithClasses)) {
                filterConfig.removeElementsWithClasses.forEach(className => {
                    const selector = className.startsWith('.') ? className : '.' + className;
                    const escapedSelector = selector.replace(/([.#\[\](){}*+?^$|\\])/g, '\\$1');
                    clonedElement.querySelectorAll(`[class*="${className}"], ${escapedSelector}`).forEach(elToRemove => elToRemove.remove());
                });
            }
            if (filterConfig.removeElementsWithIds && Array.isArray(filterConfig.removeElementsWithIds)) {
                filterConfig.removeElementsWithIds.forEach(idName => {
                    const selector = idName.startsWith('#') ? idName : '#' + idName;
                    const escapedSelector = selector.replace(/([.#\[\](){}*+?^$|\\])/g, '\\$1');
                    const elToRemove = clonedElement.querySelector(escapedSelector);
                    if (elToRemove) elToRemove.remove();
                });
            }

            // Smart content detection
            if (filterConfig.smartContentDetection) {
                clonedElement.querySelectorAll('*').forEach(el => {
                    const classList = Array.from(el.classList).join(' ').toLowerCase();
                    const id = (el.id || '').toLowerCase();
                    const commonNoisePatterns = /\b(ad|ads|advertisement|banner|popup|modal|overlay|sidebar|footer|header|nav|menu|social|share|comment|related|recommend)\b/;

                    if (commonNoisePatterns.test(classList) || commonNoisePatterns.test(id)) {
                        el.remove();
                    }
                });
            }
        }

        const html = clonedElement.outerHTML;
        let turndownMd = turndownService.turndown(html);

        // Enhanced post-processing Markdown cleanup
        turndownMd = turndownMd.replace(/\[\s*]\(\s*\)/g, ''); // Remove completely empty links
        turndownMd = turndownMd.replace(/\[\s*]\((#|javascript:[^)]*|mailto:|tel:)\)/g, ''); // Remove empty/junk links
        turndownMd = turndownMd.replace(/\[([^\]]+)]\(\s*\)/g, '$1'); // Remove links with text but no href
        turndownMd = turndownMd.replace(/\[([^\]]+)]\(\1\)/g, '$1'); // Remove redundant links where text equals URL
        turndownMd = turndownMd.replace(/!\[\s*]\(\s*\)/g, ''); // Remove empty images
        turndownMd = turndownMd.replace(/\n{3,}/g, '\n\n'); // Consolidate multiple blank lines
        turndownMd = turndownMd.replace(/^\s*\n+|\n+\s*$/g, ''); // Trim leading/trailing whitespace
        turndownMd = turndownMd.replace(/(\*\*|__)\s*\1/g, ''); // Remove empty bold/italic markers
        turndownMd = turndownMd.replace(/`\s*`/g, ''); // Remove empty code spans

        return turndownMd.trim();
    }

    function showMarkdownModal(markdown) {
        const $modal = $(`
            <div class="h2m-modal-overlay">
                <div class="h2m-modal">
                    <div class="h2m-modal-body">
                        <textarea class="h2m-markdown-area" spellcheck="false"></textarea>
                        <div class="h2m-preview"></div>
                    </div>
                    <div class="h2m-modal-footer">
                        <button class="h2m-copy">Copy to clipboard</button>
                        <button class="h2m-download">Download as MD</button>
                    </div>
                    <button class="h2m-close">${closeButtonSvgIcon}</button>
                </div>
            </div>
        `);

        const $markdownArea = $modal.find('.h2m-markdown-area');
        const $previewArea = $modal.find('.h2m-preview');
        const $copyButton = $modal.find('.h2m-copy');
        const $downloadButton = $modal.find('.h2m-download');
        const $closeButton = $modal.find('.h2m-close');

        $markdownArea.val(markdown); // Set initial value
        $previewArea.html(marked.parse(markdown));

        $markdownArea.on('input', function () { $previewArea.html(marked.parse($(this).val())); });
        const closeModal = () => { $modal.remove(); $(document).off('keydown.h2mModalGlobal'); };
        $modal.on('keydown', function (e) { if (e.key === 'Escape') closeModal(); });
        $(document).on('keydown.h2mModalGlobal', function (e) { if (e.key === 'Escape' && $('.h2m-modal-overlay').length > 0) closeModal(); });
        $copyButton.on('click', function () { GM_setClipboard($markdownArea.val()); $(this).text('Copied!'); setTimeout(() => $(this).text('Copy to clipboard'), 1000); });
        $downloadButton.on('click', function () {
            const md = $markdownArea.val(); const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
            const url = URL.createObjectURL(blob); const a = document.createElement('a');
            a.href = url; const safeTitle = (document.title.replace(/[\\/:*?"<>|]/g, '_') || 'untitled');
            a.download = `${safeTitle}-${new Date().toISOString().replace(/:/g, '-')}.md`;
            document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
        });
        $closeButton.on('click', closeModal);

        let isScrolling = false;
        function syncScroll(source, target) {
            if (isScrolling) { isScrolling = false; return; } isScrolling = true;
            const sh = source.scrollHeight - source.offsetHeight; if (sh <= 0) { isScrolling = false; return; }
            const scrollPercentage = source.scrollTop / sh;
            target.scrollTop = scrollPercentage * (target.scrollHeight - target.offsetHeight);
            setTimeout(() => isScrolling = false, 50);
        }
        $markdownArea.on('scroll', () => syncScroll($markdownArea[0], $previewArea[0]));
        $previewArea.on('scroll', () => syncScroll($previewArea[0], $markdownArea[0]));
        $('body').append($modal); $markdownArea.trigger('input'); // Trigger input to ensure preview is initially synced if markdown is complex
    }

    function updateTip() {
        let message;
        if (isMultiSelectMode) {
            message = `<b>Multi-Select Mode (${selectedElements.length} selected)</b><br><b>Click</b> to add/remove element.<br>Release <b>Shift</b> to exit.<br>Press <b>Enter</b> to convert.<br><b>Esc</b> to cancel.`
        } else {
            message = '<b>Single-Select Mode</b><br>Navigate with mouse/arrows. <b>Click</b> to convert.<br>Hold <b>Shift</b> for Multi-Select.<br><b>Esc</b> to cancel.';
        }
        tip(message);
    }

    function processSelection() {
        try {
            let finalElements = isMultiSelectMode ? selectedElements : [hoveredElement];
            if (finalElements.length === 0 || (finalElements.length === 1 && !finalElements[0])) {
                tip('No element selected.', 2000);
                return;
            }

            // Sort elements by their document order (top-to-bottom)
            finalElements.sort((a, b) => {
                const position = a.compareDocumentPosition(b);
                if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
                    return 1; // a is after b
                } else if (position & Node.DOCUMENT_POSITION_PRECEDING) {
                    return -1; // a is before b
                }
                return 0;
            });

            const markdown = finalElements.map(el => convertToMarkdown(el)).join('\n\n---\n\n');

            if (markdown.trim()) {
                showMarkdownModal(markdown);
            } else {
                tip('所选元素没有有效内容', 2000);
            }
        } catch (err) {
            console.error("[HTML to MD] Error during conversion or showing modal:", err);
            alert("Error processing selection. Check console for details.");
        } finally {
            endSelecting();
        }
    }

    function startSelecting() {
        if (isSelecting) return;
        $('body').addClass('h2m-no-scroll');
        isSelecting = true;
        isMultiSelectMode = false;
        selectedElements = [];
        hoveredElement = document.body.firstElementChild || document.body;
        if (hoveredElement) {
            $(hoveredElement).addClass('h2m-selection-box');
        }
        updateTip();
    }
    function endSelecting() { if (!isSelecting) return; isSelecting = false; isMultiSelectMode = false; $('.h2m-selection-box').removeClass('h2m-selection-box'); $('.h2m-selected-item').removeClass('h2m-selected-item'); $('body').removeClass('h2m-no-scroll'); $('#h2m-tip-instance').remove(); hoveredElement = null; selectedElements = []; }
    function isContentElement(el) {
        const contentTags = ['P', 'DIV', 'ARTICLE', 'SECTION', 'MAIN', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'PRE', 'CODE', 'TABLE'];
        return contentTags.includes(el.tagName) || el.textContent.trim().length > 20;
    }

    function isValidElement(el) {
        if (!el || ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(el.tagName)) return false;
        const rect = el.getBoundingClientRect();
        return rect.width > 0 && rect.height > 0;
    }

    // Use an ID for the tip element to increase specificity without !important.
    function tip(message, timeout = null) {
        $('#h2m-tip-instance').remove(); // Remove any existing tip by its unique ID
        const $t = $('<div>')
            .attr('id', 'h2m-tip-instance') // Assign a unique ID for high-specificity styling
            .html(message)
            .appendTo('body')
            .hide()
            .fadeIn(200);
        if (timeout !== null) {
            setTimeout(() => { $t.fadeOut(200, () => $t.remove()); }, timeout);
        }
    }

    function handleKeyboardNavigation(e) {
        if (!isSelecting || !hoveredElement) return;
        e.preventDefault();
        let newEl = hoveredElement;

        switch (e.key) {
            case 'Escape': endSelecting(); return;
            case 'Enter':
                if (isMultiSelectMode) {
                    processSelection();
                }
                return;
            case 'ArrowUp':
                newEl = hoveredElement.parentElement || hoveredElement;
                if (['HTML', 'BODY'].includes(newEl.tagName)) {
                    newEl = newEl.firstElementChild || newEl;
                }
                break;
            case 'ArrowDown':
                newEl = hoveredElement.firstElementChild || hoveredElement;
                break;
            case 'ArrowLeft': {
                let p = hoveredElement.previousElementSibling;
                if (p) {
                    newEl = p;
                    while (newEl.lastElementChild && !isContentElement(newEl)) {
                        newEl = newEl.lastElementChild;
                    }
                } else if (hoveredElement.parentElement && !['BODY', 'HTML'].includes(hoveredElement.parentElement.tagName)) {
                    newEl = hoveredElement.parentElement;
                }
                break;
            }
            case 'ArrowRight': {
                let n = hoveredElement.nextElementSibling;
                if (n) {
                    newEl = n;
                    while (newEl.firstElementChild && !isContentElement(newEl)) {
                        newEl = newEl.firstElementChild;
                    }
                } else if (hoveredElement.parentElement && !['BODY', 'HTML'].includes(hoveredElement.parentElement.tagName)) {
                    newEl = hoveredElement.parentElement;
                }
                break;
            }
            default: return;
        }

        if (newEl && newEl !== hoveredElement && isValidElement(newEl)) {
            $(hoveredElement).removeClass('h2m-selection-box');
            hoveredElement = newEl;
            $(hoveredElement).addClass('h2m-selection-box');
        }
    }
    function handleMouseWheelNavigation(e) {
        if (!isSelecting || !hoveredElement) return; e.preventDefault(); let newEl = hoveredElement;
        if (e.originalEvent.deltaY < 0) { newEl = hoveredElement.parentElement || hoveredElement; if (['HTML', 'BODY'].includes(newEl.tagName)) newEl = newEl.firstElementChild || newEl; }
        else { newEl = hoveredElement.firstElementChild || hoveredElement; }
        if (newEl && newEl !== hoveredElement) { $(hoveredElement).removeClass('h2m-selection-box'); hoveredElement = newEl; $(hoveredElement).addClass('h2m-selection-box'); }
    }
    $(document).on('keydown.h2m', function (e) {
        if (e.key.toUpperCase() === shortCutConfig.Key.toUpperCase() &&
            e.ctrlKey === shortCutConfig.Ctrl &&
            e.altKey === shortCutConfig.Alt &&
            e.shiftKey === shortCutConfig.Shift) {
            e.preventDefault();
            if (isSelecting) {
                endSelecting();
            } else {
                startSelecting();
            }
            return;
        }

        if (isSelecting) {
            if (e.key === 'Shift' && !isMultiSelectMode) {
                isMultiSelectMode = true;
                updateTip();
            }
            handleKeyboardNavigation(e);
        }
    }).on('keyup.h2m', function(e) {
        if (isSelecting && e.key === 'Shift') {
            isMultiSelectMode = false;
            updateTip();
        }
    }).on('mouseover.h2m', function (e) {
        if (isSelecting && hoveredElement !== e.target && !$(e.target).closest('#h2m-tip-instance, .h2m-modal-overlay').length && isValidElement(e.target)) {
            $(hoveredElement).removeClass('h2m-selection-box');
            hoveredElement = e.target;
            $(hoveredElement).addClass('h2m-selection-box');
        }
    }).on('wheel.h2m', function (e) {
        if (isSelecting) handleMouseWheelNavigation(e);
    }).on('mousedown.h2m', function (e) {
        if (isSelecting && hoveredElement && $(e.target).closest('#h2m-tip-instance, .h2m-modal-overlay').length === 0) {
            e.preventDefault();
            e.stopPropagation();

            if (isMultiSelectMode) {
                const index = selectedElements.indexOf(hoveredElement);
                if (index > -1) {
                    selectedElements.splice(index, 1);
                    $(hoveredElement).removeClass('h2m-selected-item');
                } else {
                    selectedElements.push(hoveredElement);
                    $(hoveredElement).addClass('h2m-selected-item');
                }
                updateTip();
            } else {
                processSelection();
            }
        }
    });
    GM_registerMenuCommand('开始选择 / Start Selection', startSelecting);
    GM_registerMenuCommand('配置过滤规则 / Configure Filters', () => {
        const currentFilters = JSON.stringify(filterConfig || DEFAULT_FILTER_CONFIG, null, 2);
        const newFiltersStr = prompt("编辑过滤规则 (JSON):\n支持的配置项:\n- removeTags: 要移除的HTML标签\n- removeAttributes: 要移除的属性\n- keepAttributesOnTags: 特定标签保留的属性\n- removeElementsWithClasses: 要移除的CSS类\n- removeElementsWithIds: 要移除的ID\n- smartContentDetection: 智能内容检测\n- preserveCodeBlocks: 保留代码块", currentFilters);
        if (newFiltersStr) {
            try {
                const newFilters = JSON.parse(newFiltersStr);
                filterConfig = { ...DEFAULT_FILTER_CONFIG, ...newFilters };
                GM_setValue('filterConfig', JSON.stringify(filterConfig));
                alert("过滤规则已更新!页面将刷新以应用新规则。");
                location.reload();
            } catch (err) {
                alert("无效的JSON格式!规则未更新。\n" + err.message);
            }
        }
    });

    GM_registerMenuCommand('重置为默认配置 / Reset to Default', () => {
        if (confirm('确定要重置所有配置为默认值吗?')) {
            GM_setValue('filterConfig', JSON.stringify(DEFAULT_FILTER_CONFIG));
            GM_setValue('shortCutConfig', JSON.stringify(DEFAULT_SHORTCUT_CONFIG));
            alert('配置已重置!页面将刷新。');
            location.reload();
        }
    });

    // --- CSS Styles ---
    GM_addStyle(`
        .h2m-selection-box {
            outline: 2px dashed #0B57D0 !important;
            background-color: rgba(11, 87, 208, 0.1) !important;
            box-shadow: 0 0 0 9999px rgba(0,0,0,0.05), inset 0 0 0 1px rgba(11, 87, 208, 0.3) !important;
            position: relative;
            z-index: 9999998;
            transition: all 0.2s ease-in-out !important;
        }
        .h2m-selected-item {
            outline: 2px solid #D00B0B !important; /* Solid red outline for selected items */
            background-color: rgba(208, 11, 11, 0.15) !important; /* Light red background */
            box-shadow: 0 0 0 9999px rgba(0,0,0,0.05), inset 0 0 0 1px rgba(208, 11, 11, 0.4) !important;
        }
        .h2m-selection-box::before {
            content: attr(tagName) ' - ' attr(class);
            position: absolute;
            top: -25px;
            left: 0;
            background: #0B57D0;
            color: white;
            padding: 2px 8px;
            font-size: 12px;
            border-radius: 3px;
            z-index: 10000000;
            font-family: monospace;
            white-space: nowrap;
            max-width: 200px;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .h2m-no-scroll { overflow: hidden !important; }
        .h2m-modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); z-index: 9999999; display: flex; align-items: center; justify-content: center; }
        .h2m-modal {
            width: 90%; height: 85%; max-width: 1600px; max-height: 95vh;
            background: #FFFFFF; border-radius: 16px;
            box-shadow: 0 8px 12px rgba(0,0,0,0.15), 0 4px 8px rgba(0,0,0,0.1);
            display: flex; flex-direction: column; padding: 0; position: relative; overflow: hidden;
        }
        .h2m-modal-body { flex-grow: 1; display: flex; flex-direction: row; overflow: hidden; border-top-left-radius: 16px; border-top-right-radius: 16px; }
        .h2m-modal-footer {
            flex-shrink: 0; padding: 12px 24px;
            background-color: #F8F9FA; border-top: 1px solid #DEE2E6;
            display: flex; justify-content: flex-end; align-items: center; gap: 12px; /* Ensure vertical alignment and gap */
            border-bottom-left-radius: 16px; border-bottom-right-radius: 16px;
            position: relative;
        }
        .h2m-modal textarea.h2m-markdown-area, .h2m-modal .h2m-preview {
            flex: 1; height: 100%; padding: 20px 24px; box-sizing: border-box;
            overflow-y: auto; border: none; font-size: 14px; line-height: 1.6; margin: 0;
        }
        .h2m-modal textarea.h2m-markdown-area { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; border-right: 1px solid #DCDCDC; resize: none; color: #333; background-color: #FAFAFA; }
        .h2m-modal textarea.h2m-markdown-area:focus { outline: none; box-shadow: none; }
        .h2m-modal .h2m-preview { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; background-color: #FFFFFF !important; color: #1C1B1F !important; }
        .h2m-modal .h2m-preview * { color: inherit !important; background-color: transparent !important; font-family: inherit !important; font-size: inherit !important; line-height: inherit !important; margin: 0; padding: 0; border: 0; }
        .h2m-modal .h2m-preview p { margin-bottom: 1em; }
        .h2m-modal .h2m-preview h1, .h2m-modal .h2m-preview h2, .h2m-modal .h2m-preview h3, .h2m-modal .h2m-preview h4, .h2m-modal .h2m-preview h5, .h2m-modal .h2m-preview h6 { margin-top: 1.5em; margin-bottom: 0.5em; font-weight: 600; line-height: 1.2; }
        .h2m-modal .h2m-preview h1 { font-size: 2em; } .h2m-modal .h2m-preview h2 { font-size: 1.75em; } .h2m-modal .h2m-preview h3 { font-size: 1.5em; } .h2m-modal .h2m-preview h4 { font-size: 1.25em; } .h2m-modal .h2m-preview h5 { font-size: 1.125em; } .h2m-modal .h2m-preview h6 { font-size: 1em; }
        .h2m-modal .h2m-preview a, .h2m-modal .h2m-preview a:visited { color: #0B57D0 !important; text-decoration: none !important; }
        .h2m-modal .h2m-preview a:hover, .h2m-modal .h2m-preview a:focus { text-decoration: underline !important; }
        .h2m-modal .h2m-preview ul, .h2m-modal .h2m-preview ol { margin-bottom: 1em; padding-left: 2em; }
        .h2m-modal .h2m-preview li { margin-bottom: 0.25em; }
        .h2m-modal .h2m-preview ul li::marker, .h2m-modal .h2m-preview ol li::marker { color: #1C1B1F; }
        .h2m-modal .h2m-preview blockquote { border-left: 4px solid #CAC4D0; padding: 0.5em 1em; margin: 1em 0; color: #49454F !important; background-color: #F5F3F7 !important; }
        .h2m-modal .h2m-preview blockquote p { margin-bottom: 0.5em; } .h2m-modal .h2m-preview blockquote p:last-child { margin-bottom: 0; }
        .h2m-modal .h2m-preview code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; background-color: #E8DEF8 !important; color: #1D192B !important; padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; }
        .h2m-modal .h2m-preview pre { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; background-color: #202124 !important; color: #E8EAED !important; padding: 1em; margin: 1em 0; border-radius: 8px; overflow-x: auto; font-size: 0.9em; line-height: 1.45; }
        .h2m-modal .h2m-preview pre code { background-color: transparent !important; color: inherit !important; padding: 0; border-radius: 0; font-size: inherit; }
        .h2m-modal .h2m-preview table { width: auto; max-width: 100%; border-collapse: collapse; margin: 1em 0; border: 1px solid #CAC4D0; }
        .h2m-modal .h2m-preview th, .h2m-modal .h2m-preview td { border: 1px solid #CAC4D0; padding: 0.5em 0.75em; text-align: left; }
        .h2m-modal .h2m-preview th { background-color: #F5F3F7 !important; font-weight: 600; }
        .h2m-modal .h2m-preview hr { border: none; border-top: 1px solid #CAC4D0; margin: 2em 0; }
        .h2m-modal .h2m-preview img { max-width: 100%; height: auto; border-radius: 8px; margin: 1em 0; display: block; }

        .h2m-modal-footer button,
        .h2m-modal-footer button.h2m-copy,
        .h2m-modal-footer button.h2m-download {
            position: static !important;
            display: inline-flex !important;
            background-color: #0B57D0 !important; color: #FFFFFF !important; border: none;
            border-radius: 20px; padding: 0 24px; font-size: 14px; font-weight: 500;
            line-height: 1; text-align: center; text-decoration: none;
            align-items: center; justify-content: center;
            height: 40px; min-width: 80px; box-sizing: border-box; cursor: pointer;
            box-shadow: 0 1px 2px rgba(0,0,0,0.15), 0 1px 3px rgba(0,0,0,0.1);
            transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
            margin: 0;
        }
        .h2m-modal-footer button:hover,
        .h2m-modal-footer button.h2m-copy:hover,
        .h2m-modal-footer button.h2m-download:hover {
            background-color: #0A50BF !important;
            box-shadow: 0 2px 4px rgba(0,0,0,0.15), 0 2px 6px rgba(0,0,0,0.1);
        }

        .h2m-modal .h2m-close { position: absolute; top: 12px; right: 12px; width: 40px; height: 40px; background-color: transparent !important; border-radius: 50%; border: none; display: flex; justify-content: center; align-items: center; cursor: pointer; padding: 0; box-shadow: none !important; z-index: 20; transition: opacity 0.2s ease-in-out; }
        .h2m-modal .h2m-close svg { width: 24px; height: 24px; display: block; }
        .h2m-modal .h2m-close svg path { fill: #B3261E !important; transition: fill 0.2s ease-in-out; }
        .h2m-modal .h2m-close:hover svg path { fill: #9E221A !important; }
        .h2m-modal .h2m-close:hover { opacity: 0.85; }

        /* Use a high-specificity ID selector to style the tip box, avoiding !important */
        #h2m-tip-instance {
            position: fixed;
            top: 20px;
            right: 20px;
            background-color: rgba(255,255,255,0.95);
            color: #202124; /* High specificity from ID selector makes !important unnecessary */
            border: 1px solid #DCDCDC;
            padding: 10px 15px;
            z-index: 10000000;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            max-width: 300px;
            font-family: sans-serif;
            font-size: 14px;
        }
        #h2m-tip-instance h1, #h2m-tip-instance h2, #h2m-tip-instance h3 { margin-top: 0.5em; margin-bottom: 0.2em; font-weight: 600; }
        #h2m-tip-instance ul { margin-left: 20px; padding-left: 0; }
        #h2m-tip-instance li { margin-bottom: 0.3em; }
    `);

    console.log('[HTML Content to Markdown] Script loaded. Version 0.2.0. Shortcut:', shortCutConfig, "Filters:", filterConfig);
    if (!TurndownPluginGfmService || typeof TurndownPluginGfmService.gfm !== 'function') {
        console.error("[HTML to MD] Turndown GFM plugin not loaded correctly!");
        alert("[HTML to MD] Error: GFM plugin failed to load. Some Markdown features might not work correctly.");
    }
    if (typeof marked === 'undefined' || typeof marked.parse !== 'function') {
        console.error("[HTML to MD] Marked library not loaded correctly!");
        alert("[HTML to MD] Error: Markdown preview library (Marked) failed to load.");
    }

})();