微信公众号编辑器HTML工具

在微信公众号编辑器中添加HTML代码查看和编辑功能

// ==UserScript==
// @name         微信公众号编辑器HTML工具
// @namespace    http://tampermonkey.net/
// @version      0.0.2
// @description  在微信公众号编辑器中添加HTML代码查看和编辑功能
// @author       liudonghua123
// @match        https://mp.weixin.qq.com/cgi-bin/appmsg*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js
// @license MIT
// ==/UserScript==

(async function() {
    'use strict';
    console.info(`微信公众号编辑器HTML工具`);
    // 等待编辑器加载完成
    async function waitForEditor() {
        return new Promise((resolve) => {
            console.log('微信公众号HTML工具: 等待编辑器加载...');
            const checkEditor = setInterval(() => {
                if (window.__MP_Editor_JSAPI__) {
                    clearInterval(checkEditor);
                    console.log('微信公众号HTML工具: 编辑器已加载');
                    resolve();
                }
            }, 1000);
        });
    }

    async function waitForToolbar() {
        return new Promise((resolve) => {
            console.log('微信公众号HTML工具: 等待工具栏加载...');
            const checkToolbar = setInterval(() => {
                const toolbar = document.querySelector('.edui-toolbar.edui-toolbar-primary');
                if (toolbar) {
                    clearInterval(checkToolbar);
                    console.log('微信公众号HTML工具: 工具栏已加载');
                    resolve(toolbar);
                }
            }, 500);
        });
    }

    // 监听工具栏变化,确保按钮始终存在
    function observeToolbar(toolbar) {
        // 清除现有的观察器
        if (window.htmlToolObserver) {
            window.htmlToolObserver.disconnect();
        }

        const observer = new MutationObserver((mutations) => {
            // 检查按钮是否已存在
            const existingButton = toolbar.querySelector('.edui-for-htmlcode');
            if (!existingButton) {
                console.log('微信公众号HTML工具: 检测到工具栏变化,重新添加HTML按钮');
                addHTMLButton(toolbar);
            }
        });

        observer.observe(toolbar, {
            childList: true,
            subtree: true
        });

        // 保存观察器引用,防止被垃圾回收
        window.htmlToolObserver = observer;

        // 定期检查按钮是否存在(备用机制)
        const interval = setInterval(() => {
            const existingButton = toolbar.querySelector('.edui-for-htmlcode');
            if (!existingButton) {
                console.log('微信公众号HTML工具: 定期检查发现按钮缺失,重新添加HTML按钮');
                addHTMLButton(toolbar);
            }
        }, 2000);

        // 保存定时器引用
        window.htmlToolInterval = interval;
    }

    // 页面可见性变化时检查按钮
    document.addEventListener('visibilitychange', () => {
        if (!document.hidden) {
            // 页面变为可见时,延迟检查按钮
            setTimeout(() => {
                checkAndAddButton();
            }, 500);
        }
    });

    // 窗口焦点变化时检查按钮
    window.addEventListener('focus', () => {
        setTimeout(() => {
            checkAndAddButton();
        }, 500);
    });

    // 检查并添加按钮的通用函数
    async function checkAndAddButton() {
        try {
            const toolbar = await waitForToolbar();
            const existingButton = toolbar.querySelector('.edui-for-htmlcode');
            if (!existingButton) {
                console.log('微信公众号HTML工具: 检查发现按钮缺失,重新添加HTML按钮');
                addHTMLButton(toolbar);
            }
        } catch (error) {
            console.error('微信公众号HTML工具: 检查按钮失败', error);
        }
    }

    async function initHTMLTool() {
        try {
            await waitForEditor();
            const toolbar = await waitForToolbar();
            addHTMLButton(toolbar);
            observeToolbar(toolbar);

            // 定期重新检查工具栏(防止观察器失效)
            setInterval(async () => {
                try {
                    const currentToolbar = await waitForToolbar();
                    const existingButton = currentToolbar.querySelector('.edui-for-htmlcode');
                    if (!existingButton) {
                        console.log('微信公众号HTML工具: 定期重新检查发现按钮缺失,重新添加HTML按钮');
                        addHTMLButton(currentToolbar);
                        // 重新绑定观察器
                        observeToolbar(currentToolbar);
                    }
                } catch (error) {
                    console.error('微信公众号HTML工具: 定期检查失败', error);
                }
            }, 5000);
        } catch (error) {
            console.error('微信公众号HTML工具: 初始化失败', error);
            createNotification('工具初始化失败: ' + error.message, 'error');
        }
    }

    function addHTMLButton(toolbar) {
        // 检查按钮是否已存在
        const existingButton = toolbar.querySelector('.edui-for-htmlcode');
        if (existingButton) {
            console.log('微信公众号HTML工具: HTML按钮已存在,无需重复添加');
            return;
        }

        // 创建HTML按钮
        const htmlButton = document.createElement('div');
        htmlButton.className = 'edui-box edui-button edui-default edui-for-htmlcode';
        htmlButton.innerHTML = `
            <div data-tooltip="HTML代码" class="js_tooltip edui-default">
                <div class="edui-button-wrap edui-default">
                    <div class="edui-box edui-button-body edui-default">
                        <div class="edui-box edui-icon edui-default" style="font-size: 16px; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
                            <span>📄</span>
                        </div>
                    </div>
                </div>
            </div>
        `;

        // 添加到工具栏末尾
        toolbar.appendChild(htmlButton);

        // 绑定点击事件
        htmlButton.addEventListener('click', showHTMLDialog);

        // 创建一键整理图片按钮
        const formatImageButton = document.createElement('div');
        formatImageButton.className = 'edui-box edui-button edui-default edui-for-formatimage';
        formatImageButton.innerHTML = `
            <div data-tooltip="一键整理图片" class="js_tooltip edui-default">
                <div class="edui-button-wrap edui-default">
                    <div class="edui-box edui-button-body edui-default">
                        <div class="edui-box edui-icon edui-default" style="font-size: 16px; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
                            <span>🖼️</span>
                        </div>
                    </div>
                </div>
            </div>
        `;

        // 添加到HTML按钮右边
        toolbar.appendChild(formatImageButton);

        // 绑定点击事件
        formatImageButton.addEventListener('click', formatImages);

        console.log('微信公众号HTML工具: HTML按钮和一键整理图片按钮添加成功');
    }

    async function getEditorContent() {
        return new Promise((resolve, reject) => {
            window.__MP_Editor_JSAPI__.invoke({
                apiName: 'mp_editor_get_content',
                sucCb: (res) => {
                    console.log('微信公众号HTML工具: 获取内容成功', res);
                    resolve(res.content);
                },
                errCb: (err) => {
                    console.error('微信公众号HTML工具: 获取内容失败', err);
                    reject(err);
                }
            });
        });
    }

    async function setEditorContent(content) {
        return new Promise((resolve, reject) => {
            window.__MP_Editor_JSAPI__.invoke({
                apiName: 'mp_editor_set_content',
                apiParam: {
                    content: content
                },
                sucCb: (res) => {
                    console.log('微信公众号HTML工具: 设置内容成功', res);
                    resolve(res);
                },
                errCb: (err) => {
                    console.error('微信公众号HTML工具: 设置内容失败', err);
                    reject(err);
                }
            });
        });
    }

    async function loadMonaco() {
        return new Promise((resolve, reject) => {
            if (window.monaco) {
                resolve();
                return;
            }

            const script = document.createElement('script');
            script.src = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js';
            script.onload = () => {
                require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' }});
                require(['vs/editor/editor.main'], () => {
                    resolve();
                }, reject);
            };
            script.onerror = reject;
            document.head.appendChild(script);
        });
    }

    async function showHTMLDialog() {
        try {
            // 获取当前HTML内容
            const content = await getEditorContent();

            // 创建对话框
            const dialog = document.createElement('div');
            dialog.id = 'html-editor-dialog';
            dialog.innerHTML = `
                <div class="html-editor-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999;"></div>
                <div class="html-editor-container" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80%; height: 70%; background: white; border-radius: 8px; z-index: 10000; display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0,0,0,0.15); border: 1px solid #e0e0e0;">
                    <div class="html-editor-header" style="padding: 16px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; background: #f8f8f8; border-radius: 8px 8px 0 0;">
                        <h3 style="margin: 0; font-weight: 500; color: #333;">HTML代码编辑器</h3>
                        <div style="display: flex; gap: 8px;">
                            <button class="html-editor-maximize" style="background: none; border: none; font-size: 16px; cursor: pointer; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;" title="最大化">□</button>
                            <button class="html-editor-close" style="background: none; border: none; font-size: 24px; cursor: pointer; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;">×</button>
                        </div>
                    </div>
                    <div class="html-editor-body" style="flex: 1; padding: 0; overflow: hidden;">
                        <div id="monaco-editor" style="width: 100%; height: 100%;"></div>
                    </div>
                    <div class="html-editor-footer" style="padding: 16px; border-top: 1px solid #e0e0e0; display: flex; justify-content: flex-end; gap: 12px; background: #f8f8f8; border-radius: 0 0 8px 8px;">
                        <button id="format-btn" style="padding: 8px 16px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-size: 14px;">格式化</button>
                        <button id="save-btn" style="padding: 8px 16px; background: #07c160; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">保存</button>
                    </div>
                </div>
            `;

            // 添加到页面
            document.body.appendChild(dialog);

            // 加载Monaco Editor
            await loadMonaco();

            // 初始化Monaco Editor
            const editor = monaco.editor.create(document.getElementById('monaco-editor'), {
                value: content || '',
                language: 'html',
                theme: 'vs-light',
                automaticLayout: true,
                minimap: {
                    enabled: true
                },
                fontSize: 14,
                scrollBeyondLastLine: false,
                wordWrap: 'on'
            });

            // 保存编辑器实例到对话框上,方便后续访问
            dialog.editorInstance = editor;

            // 绑定事件
            dialog.querySelector('.html-editor-close').addEventListener('click', () => {
                editor.dispose();
                document.body.removeChild(dialog);
            });

            dialog.querySelector('.html-editor-overlay').addEventListener('click', () => {
                editor.dispose();
                document.body.removeChild(dialog);
            });

            // 最大化功能
            let isMaximized = false;
            const container = dialog.querySelector('.html-editor-container');
            const maximizeBtn = dialog.querySelector('.html-editor-maximize');

            maximizeBtn.addEventListener('click', () => {
                if (isMaximized) {
                    // 恢复原状
                    container.style.width = '80%';
                    container.style.height = '70%';
                    container.style.top = '50%';
                    container.style.left = '50%';
                    container.style.transform = 'translate(-50%, -50%)';
                    maximizeBtn.textContent = '□';
                    maximizeBtn.title = '最大化';
                    isMaximized = false;
                } else {
                    // 最大化
                    container.style.width = '95%';
                    container.style.height = '90%';
                    container.style.top = '5%';
                    container.style.left = '2.5%';
                    container.style.transform = 'none';
                    maximizeBtn.textContent = '❐';
                    maximizeBtn.title = '恢复';
                    isMaximized = true;
                }
                // 通知编辑器重新布局
                setTimeout(() => editor.layout(), 100);
            });

            // 格式化功能
            dialog.querySelector('#format-btn').addEventListener('click', () => {
                const currentValue = editor.getValue();
                // 简单的HTML格式化
                let formatted = currentValue.replace(/></g, '>\n<');
                editor.setValue(formatted);
            });

            // 保存功能
            dialog.querySelector('#save-btn').addEventListener('click', async () => {
                const html = editor.getValue();
                try {
                    await setEditorContent(html);
                    createNotification('保存成功', 'success');
                } catch (e) {
                    console.error('微信公众号HTML工具: 保存失败', e);
                    createNotification('保存失败: ' + e.message, 'error');
                }
            });
        } catch (error) {
            console.error('微信公众号HTML工具: 显示对话框失败', error);
            createNotification('获取内容失败: ' + error.message, 'error');
        }
    }

    // 创建通知banner
    function createNotification(message, type = 'info') {
        // 移除现有的通知
        const existingNotification = document.getElementById('html-editor-notification');
        if (existingNotification) {
            existingNotification.remove();
        }

        const notification = document.createElement('div');
        notification.id = 'html-editor-notification';
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            padding: 12px 24px;
            border-radius: 4px;
            color: white;
            font-size: 14px;
            z-index: 10001;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            transition: opacity 0.3s ease-in-out;
            max-width: 80%;
            text-align: center;
        `;

        // 根据类型设置样式
        switch (type) {
            case 'success':
                notification.style.backgroundColor = '#07c160';
                break;
            case 'error':
                notification.style.backgroundColor = '#fa5151';
                break;
            case 'warning':
                notification.style.backgroundColor = '#ffc300';
                notification.style.color = '#333';
                break;
            default:
                notification.style.backgroundColor = '#000000';
        }

        notification.textContent = message;
        document.body.appendChild(notification);

        // 3秒后自动移除
        setTimeout(() => {
            if (notification.parentNode) {
                notification.style.opacity = '0';
                setTimeout(() => {
                    if (notification.parentNode) {
                        notification.remove();
                    }
                }, 300);
            }
        }, 3000);

        return notification;
    }

    // 一键整理图片功能
    async function formatImages() {
        try {
            // 获取当前HTML内容
            const content = await getEditorContent();

            // 处理HTML内容
            let formattedContent = content;

            // 统计修改数量
            let imgModifiedCount = 0;
            let sectionRemovedCount = 0;

            // 2.1 找到在<section style="text-align: center;" nodeleaf=""> </section> 中的 带有class="rich_pages wxw-img" 没有 style的 img,添加样式
            formattedContent = formattedContent.replace(
                /(<section style="text-align: center;" nodeleaf="">)([\s\S]*?)(<\/section>)/g,
                (sectionMatch, openTag, content, closeTag) => {
                    // 处理section中的img标签
                    const processedContent = content.replace(
                        /<img(?![^>]*style="[^"]*border-radius[^"]*")([^>]*class="rich_pages wxw-img"[^>]*>)/g,
                        (imgMatch, imgTag) => {
                            imgModifiedCount++;
                            return `<img style="border-radius: 9px;box-shadow: rgb(210, 210, 210) 0px 0px 0.5em 0px;background-color: transparent;"${imgTag}`;
                        }
                    );
                    return openTag + processedContent + closeTag;
                }
            );

            // 2.2 删除可能的<section ...><figure ...><span leaf=""><br ...></span>...</figure></section>
            const removeSectionRegex = /<section[^>]*?><figure[^>]*?><span leaf=""><br[^>]*?><\/span>(<figcaption[^>]*?>.*?<\/figcaption>)?<\/figure><\/section>/g;
            let removeSectionMatch;
            while ((removeSectionMatch = removeSectionRegex.exec(formattedContent)) !== null) {
                sectionRemovedCount++;
            }
            formattedContent = formattedContent.replace(removeSectionRegex, '');

            // 设置处理后的内容
            await setEditorContent(formattedContent);
            createNotification(`图片整理完成,修改了 ${imgModifiedCount} 处图片,删除了 ${sectionRemovedCount} 处多余内容`, 'success');
        } catch (error) {
            console.error('微信公众号HTML工具: 图片整理失败', error);
            createNotification('图片整理失败: ' + error.message, 'error');
        }
    }


    await initHTMLTool();
})();