【脚本!?但安全】一个自用的纯编辑向油猴callouts插件~适合喜欢快速插入醒目词条的佬们~

[!success]Callouts插件效果
RT↑,RT↓


:tieba_018:

之前的插件不知道为啥挂了……自己重新弄了一个~

[!note]由三部分组成
一、前面的是13种Obsidian Callouts,基于始皇帖子~
二、是三个常用表情:tieba_022::tieba_018::distorted_face:,我太喜欢用了哈哈哈……(自用的话可以替换或增加代码对应部分)
三、是detials和spoiler常用的两个隐藏和模糊功能,选中文字后直接点即可~

[!abstract]使用方法~
1、浏览器安装油猴插件(具体方法一般为浏览器-扩展-管理扩展-获取扩展,搜索即可安装)


2、打开浏览器插件-篡改猴-添加新脚本-复制代码全部覆盖-保存-刷新页面即可

添加新脚本-贴代码



全选-粘贴↓下方代码,全覆盖-保存~

脚本代码

// ==UserScript==
// @name         Discourse Callout 插入工具栏(Linux.do回复工具栏)
// @namespace    https://linux.do/
// @version      3.5
// @description  添加 Callout 按钮,并支持 details/spoiler 插入、层级化Callout插入(仅当前行有Callout时换行)- 优化性能和错误处理
// @match        https://linux.do/*
// @match        https://idcflare.com/*
// @grant        none
// ==/UserScript==

(function () {
    "use strict";

    const CALLOUTS = [
        { type: "note",      color: "#4c8df6", icon: "💬" },
        { type: "abstract",  color: "#8bd8d1", icon: "📄" },
        { type: "info",      color: "#0099ff", icon: "💡" },
        { type: "todo",      color: "#5ab1ff", icon: "⭕" },
        { type: "tip",       color: "#5dd6c0", icon: "🔥" },
        { type: "success",   color: "#55cc88", icon: "✔" },
        { type: "question",  color: "#ffb866", icon: "❓" },
        { type: "warning",   color: "#ff9933", icon: "⚠" },
        { type: "failure",   color: "#ff6666", icon: "❌" },
        { type: "danger",    color: "#ff4d4d", icon: "⚡" },
        { type: "bug",       color: "#ff6699", icon: "🐞" },
        { type: "example",   color: "#aa88ff", icon: "📘" },
        { type: "quote",     color: "#aaaaaa", icon: "❝" }
    ];

    const EMOJI_CONFIGS = [
        { text: "😏 眯眼笑", insertText: ":tieba_022:\n" },
        { text: "😳 干瞪眼", insertText: ":distorted_face:\n" },
        { text: "🤪 装蠢", insertText: ":tieba_018:\n" }
    ];

    // 缓存变量
    let cachedTextarea = null;
    let cacheTime = 0;
    let currentObserver = null;
    const CACHE_DURATION = 1000; // 1秒缓存

    // ====== 缓存的 DOM 查询 ======
    function getCachedTextarea() {
        const now = Date.now();
        if (cachedTextarea && (now - cacheTime) < CACHE_DURATION) {
            // 验证元素是否还在 DOM 中
            if (document.contains(cachedTextarea)) {
                return cachedTextarea;
            }
        }

        cachedTextarea = document.querySelector(".d-editor-input");
        cacheTime = now;
        return cachedTextarea;
    }

    // ====== 获取当前光标位置的Callout层级 ======
    function getCurrentCalloutLevel(textarea) {
        try {
            const start = textarea.selectionStart;
            const value = textarea.value;
            const beforeCursor = value.slice(0, start);
            const lines = beforeCursor.split("\n");
            const currentLine = lines[lines.length - 1] || "";
            const prevLine = lines.length >= 2 ? lines[lines.length - 2] : "";

            // 修复正则表达式:正确转义方括号
            const calloutRegex = /^(>+)\s*\[!.*\]/;

            // 先检查当前行是否有Callout
            const currentLineMatch = currentLine.trim().match(calloutRegex);
            if (currentLineMatch) {
                return currentLineMatch[1].length;
            }

            // 再检查上一行是否有Callout
            const prevLineMatch = prevLine.trim().match(calloutRegex);
            if (prevLineMatch) {
                return prevLineMatch[1].length;
            }

            // 没有找到Callout,返回基础层级0
            return 0;
        } catch (error) {
            console.warn('Error getting callout level:', error);
            return 0;
        }
    }

    // ====== 检查当前行是否包含Callout ======
    function isCurrentLineHasCallout(textarea) {
        try {
            const start = textarea.selectionStart;
            const value = textarea.value;
            const beforeCursor = value.slice(0, start);
            const lines = beforeCursor.split("\n");
            const currentLine = lines[lines.length - 1] || "";

            // 修复正则表达式:正确转义方括号
            const calloutRegex = /^(>+)\s*\[!.*\]/;
            return !!currentLine.trim().match(calloutRegex);
        } catch (error) {
            console.warn('Error checking current line callout:', error);
            return false;
        }
    }

    // ====== 插入 Callout,仅当前行有Callout时换行 ======
    function insertCallout(textarea, type) {
        try {
            const start = textarea.selectionStart;
            const end = textarea.selectionEnd;
            const value = textarea.value;

            const before = value.slice(0, start);
            const after = value.slice(end);

            // 获取当前层级,新层级=当前层级+1
            const currentLevel = getCurrentCalloutLevel(textarea);
            const newLevel = currentLevel + 1;
            // 生成对应数量的>符号
            const gtSymbols = ">".repeat(newLevel);

            let insertText = "";

            // 仅当当前行包含Callout时,添加换行(避免同一行拼接)
            if (isCurrentLineHasCallout(textarea)) {
                insertText += "\n";
            }

            // 插入对应层级的Callout
            insertText += `${gtSymbols} [!${type}]\n`;

            textarea.value = before + insertText + after;

            const pos = before.length + insertText.length;
            textarea.selectionStart = textarea.selectionEnd = pos;

            textarea.dispatchEvent(new Event("input"));
        } catch (error) {
            console.warn('Error inserting callout:', error);
        }
    }

    // ====== 通用文本插入函数 ======
    function insertText(textarea, text) {
        try {
            const start = textarea.selectionStart;
            const end = textarea.selectionEnd;
            const before = textarea.value.slice(0, start);
            const after = textarea.value.slice(end);

            textarea.value = before + text + after;
            const pos = before.length + text.length;
            textarea.selectionStart = textarea.selectionEnd = pos;
            textarea.dispatchEvent(new Event("input"));
        } catch (error) {
            console.warn('Error inserting text:', error);
        }
    }

    // ====== details & spoiler 插入 ======
    function wrapSelection(textarea, beforeText, afterText, multiLine = false) {
        try {
            const start = textarea.selectionStart;
            const end = textarea.selectionEnd;
            const value = textarea.value;

            const selected = value.slice(start, end);

            let insertText;
            let cursorPos;

            if (selected.length > 0) {
                insertText = beforeText + selected + afterText;
                cursorPos = start + insertText.length;
            } else {
                if (multiLine) {
                    insertText = beforeText + "\n" + afterText;
                    cursorPos = start + beforeText.length + 1;
                } else {
                    insertText = beforeText + afterText;
                    cursorPos = start + beforeText.length;
                }
            }

            textarea.value = value.slice(0, start) + insertText + value.slice(end);

            textarea.selectionStart = textarea.selectionEnd = cursorPos;
            textarea.dispatchEvent(new Event("input"));
        } catch (error) {
            console.warn('Error wrapping selection:', error);
        }
    }

    // ====== 通用按钮创建函数 ======
    function createButton(config) {
        const btn = document.createElement("button");
        btn.textContent = config.text;
        btn.innerHTML = config.html || config.text;

        // 批量设置样式
        Object.assign(btn.style, {
            display: config.display || "flex",
            alignItems: "center",
            gap: "4px",
            padding: "4px 8px",
            borderRadius: "6px",
            cursor: "pointer",
            fontSize: "12px",
            color: config.color || "#fff",
            background: config.background,
            border: config.border || "none"
        });

        btn.addEventListener("click", config.onClick);
        return btn;
    }

    // ====== 创建按钮栏 ======
    function addToolbar(textarea) {
        try {
            // 检查是否已经存在工具栏(全局检查)
            if (document.querySelector(".callout-toolbar")) return;

            // 双重检查:确保 textarea 没有被标记
            if (textarea.dataset.calloutAdded) return;

            // 检查 parentNode 是否存在
            if (!textarea.parentNode) {
                console.warn('Textarea parentNode is null');
                return;
            }

            textarea.dataset.calloutAdded = "true";

            const bar = document.createElement("div");
            bar.className = "callout-toolbar"; // 添加类名便于查找和删除
            bar.dataset.toolbarId = "discourse-callout-toolbar"; // 唯一标识
            bar.style.margin = "6px 0";
            bar.style.display = "flex";
            bar.style.flexWrap = "wrap";
            bar.style.gap = "6px";

            // === Callout 按钮 ===
            CALLOUTS.forEach(({ type, color, icon }) => {
                const btn = createButton({
                    html: `${icon} ${type}`,
                    background: color,
                    border: `1px solid ${color}99`,
                    onClick: () => insertCallout(textarea, type)
                });
                bar.appendChild(btn);
            });

            // === Emoji 按钮 ===
            EMOJI_CONFIGS.forEach(config => {
                const btn = createButton({
                    text: config.text,
                    background: "#777",
                    color: "white",
                    onClick: () => insertText(textarea, config.insertText)
                });
                bar.appendChild(btn);
            });

            // === details 按钮 ===
            const detailsBtn = createButton({
                text: "📂 details",
                background: "#666",
                color: "white",
                onClick: () => wrapSelection(
                    textarea,
                    `[details="别点我"]`,
                    `\n[/details]`,
                    true
                )
            });
            bar.appendChild(detailsBtn);

            // === spoiler 按钮 ===
            const spoilerBtn = createButton({
                text: "🙈 spoiler",
                background: "#444",
                color: "white",
                onClick: () => wrapSelection(textarea, `[spoiler]`, `[/spoiler]`)
            });
            bar.appendChild(spoilerBtn);

            textarea.parentNode.insertBefore(bar, textarea);
        } catch (error) {
            console.warn('Error adding toolbar:', error);
        }
    }

    // ====== 移除工具栏 ======
    function removeToolbar() {
        try {
            // 移除所有工具栏实例
            const toolbars = document.querySelectorAll(".callout-toolbar");
            toolbars.forEach(toolbar => toolbar.remove());

            // 清除所有 textarea 的标记
            const textareas = document.querySelectorAll(".d-editor-input");
            textareas.forEach(textarea => {
                delete textarea.dataset.calloutAdded;
            });
        } catch (error) {
            console.warn('Error removing toolbar:', error);
        }
    }

    // ====== 检查是否为 markdown 模式 ======
    function isMarkdownMode() {
        try {
            // Discourse 编辑器检查方法
            const editorContainer = document.querySelector(".d-editor");
            if (!editorContainer) return false;

            // 检查是否有 ProseMirror 编辑器(富文本模式)
            const proseMirror = editorContainer.querySelector(".ProseMirror");
            if (proseMirror && proseMirror.offsetParent !== null) {
                return false; // 富文本模式
            }

            // 检查 textarea 是否可见(markdown 模式)
            const textarea = editorContainer.querySelector(".d-editor-input");
            if (textarea && textarea.offsetParent !== null) {
                return true; // markdown 模式
            }

            return false;
        } catch (error) {
            console.warn('Error checking markdown mode:', error);
            return false;
        }
    }

    // ====== 管理工具栏显示/隐藏 ======
    function manageToolbar() {
        try {
            const textarea = getCachedTextarea();
            if (!textarea) {
                removeToolbar();
                return;
            }

            if (isMarkdownMode()) {
                // markdown 模式:添加工具栏
                addToolbar(textarea);
            } else {
                // 富文本模式:移除工具栏
                removeToolbar();
            }
        } catch (error) {
            console.warn('Callout toolbar error:', error);
            // 发生错误时清理状态
            removeToolbar();
        }
    }

    // ====== 防抖函数 ======
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // ====== 侦测编辑器变化 ======
    const debouncedManageToolbar = debounce(() => {
        // 清除缓存,强制重新查询
        cachedTextarea = null;
        cacheTime = 0;
        manageToolbar();
    }, 100);

    function setupObserver() {
        try {
            // 清理现有观察器
            if (currentObserver) {
                currentObserver.disconnect();
            }

            currentObserver = new MutationObserver((mutations) => {
                let shouldUpdate = false;

                mutations.forEach(mutation => {
                    // 检查是否有编辑器相关的变化
                    if (mutation.type === 'childList') {
                        const addedNodes = Array.from(mutation.addedNodes);
                        const removedNodes = Array.from(mutation.removedNodes);

                        if (addedNodes.some(node =>
                            node.nodeType === 1 &&
                            (node.matches?.('.d-editor, .d-editor-input, .ProseMirror') ||
                             node.querySelector?.('.d-editor, .d-editor-input, .ProseMirror'))
                        )) {
                            shouldUpdate = true;
                        }

                        if (removedNodes.some(node =>
                            node.nodeType === 1 &&
                            (node.matches?.('.d-editor, .d-editor-input, .ProseMirror') ||
                             node.querySelector?.('.d-editor, .d-editor-input, .ProseMirror'))
                        )) {
                            shouldUpdate = true;
                        }
                    }

                    if (mutation.type === 'attributes' &&
                        mutation.target.matches?.('.d-editor, .d-editor-input, .ProseMirror')) {
                        shouldUpdate = true;
                    }
                });

                if (shouldUpdate) {
                    debouncedManageToolbar();
                }
            });

            currentObserver.observe(document.body, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['class', 'style', 'hidden']
            });
        } catch (error) {
            console.warn('Error setting up observer:', error);
        }
    }

    // ====== 资源清理机制 ======
    function cleanup() {
        try {
            if (currentObserver) {
                currentObserver.disconnect();
                currentObserver = null;
            }
            removeToolbar();
            cachedTextarea = null;
            cacheTime = 0;
        } catch (error) {
            console.warn('Error during cleanup:', error);
        }
    }

    // ====== 监听点击事件(切换编辑器按钮) ======
    document.addEventListener('click', (e) => {
        try {
            // 检查是否点击了编辑器相关按钮
            const target = e.target.closest('button, .btn');
            if (target && target.closest('.d-editor')) {
                // 清除缓存,延迟执行,等待 DOM 更新
                cachedTextarea = null;
                cacheTime = 0;
                setTimeout(() => {
                    manageToolbar();
                }, 150);
            }
        } catch (error) {
            console.warn('Error handling click event:', error);
        }
    });

    // ====== 页面卸载时清理 ======
    window.addEventListener('beforeunload', cleanup);

    // ====== SPA 路由变化监听 ======
    if (window.history && window.history.pushState) {
        const originalPushState = window.history.pushState;
        window.history.pushState = function(...args) {
            cleanup();
            const result = originalPushState.apply(this, args);
            // 路由变化后重新初始化
            setTimeout(() => {
                setupObserver();
                manageToolbar();
            }, 300);
            return result;
        };
    }

    // ====== 初始化 ======
    function initialize() {
        try {
            setupObserver();
            manageToolbar();
        } catch (error) {
            console.warn('Error during initialization:', error);
        }
    }

    // 页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            setTimeout(initialize, 100);
        });
    } else {
        setTimeout(initialize, 100);
    }

})();

规矩我懂的~可不能对始皇192G裸金属服务器造成负担
:tieba_022:

[!danger]-安全性评估~
:tieba_018::tieba_018:
这样应该安全吧兄弟们!?!?
:tieba_097:


19 个赞

这个好~ 很有用~

2 个赞

真住在linux的linus
:tieba_018:

2 个赞

不明觉厉

2 个赞

我之前也写过,本来准备发的,后面发现用的不多算了 :laughing:

2 个赞

:tieba_022:
看人~
发帖多的话还是有点用的,常用表情也是,每次要找有点麻烦
:tieba_026:
主要是更难的我也不会弄

1 个赞

这个可以诶

2 个赞

恭迎水哥!!
农场好友还不给我通过!!!

1 个赞

[!success] 還行,就是輸入框高度不夠時有點問題

1 个赞

[!aa]
牛逼啊大佬

[!danger]
爱你啊大佬 :tieba_022:

1 个赞

脚本只是修改了前端代码、不请求后端接口,没问题的

1 个赞

可以的蹲蹲,造福佬友

1 个赞

这个是真的很喜欢用

1 个赞

[!note]good
好用,针不错

1 个赞

是哒,喜欢小窗的有点影响……我通常全屏

发现从油猴这类小脚本开始熟悉代码也很好玩,应该是前段代码比较简单?
:tieba_018:

喜欢快速插入醒目词条?!
:tieba_018:
:tieba_018:

1 个赞

[!success]
这个有用

1 个赞

感谢佬~
之前我就不知道这种怎么写的
今天终于找到了

1 个赞

:hugs:
我也想加你农场好友

1 个赞

:tieba_022:

[!success]
只能插入一行吗,我记得应该是两行

1 个赞