【油猴脚本】Markdown Callout

原文见感谢Reno佬的代码

[!info]
@Reno 前人栽树我乘凉

油猴脚本
// ==UserScript==
// @name         Markdown Callout
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  简单的Obsidian风格Callout工具:单按钮+表格选择+智能嵌套
// @match        https://linux.do/*
// @grant        抄Reno佬的代码
// ==/UserScript==

(function() {
    'use strict';

    // ===== Data =====
    const config = {
        types: [
            ['note', '笔记', '#448aff', '1'], ['abstract', '摘要', '#00b0ff', '2'], 
            ['summary', '总结', '#00b8d4', ''], ['info', '信息', '#00bcd4', '3'],
            ['todo', '待办', '#26c6da', '4'], ['tip', '技巧', '#00c853', '5'], 
            ['hint', '窍门', '#00e676', ''], ['important', '重要', '#9c27b0', ''],
            ['success', '成功', '#4caf50', '6'], ['done', '完成', '#66bb6a', ''], 
            ['check', '检查', '#81c784', ''], ['question', '问题', '#ff9800', '7'],
            ['help', '帮助', '#ffb74d', ''], ['faq', '问答', '#ffcc02', ''],
            ['warning', '警告', '#ff9800', '8'], ['caution', '注意', '#ffc107', ''], 
            ['attention', '当心', '#ffeb3b', ''], ['failure', '失败', '#f44336', '9'],
            ['fail', '错误', '#e57373', ''], ['missing', '丢失', '#ef5350', ''],
            ['danger', '危险', '#d32f2f', '0'], ['error', '报错', '#c62828', ''],
            ['bug', '漏洞', '#e91e63', ''], ['example', '示例', '#7c4dff', ''],
            ['quote', '引用', '#9e9e9e', ''], ['cite', '引述', '#757575', '']
        ],
        shortcuts: {'1':'note','2':'abstract','3':'info','4':'todo','5':'tip','6':'success','7':'question','8':'warning','9':'failure','0':'danger'},
        chinese: {'笔记':'note','记录':'note','备注':'note','摘要':'abstract','概要':'abstract','总结':'summary','信息':'info','资讯':'info','待办':'todo','任务':'todo','技巧':'tip','提示':'tip','窍门':'hint','重要':'important','成功':'success','完成':'done','检查':'check','问题':'question','帮助':'help','问答':'faq','警告':'warning','注意':'caution','当心':'attention','失败':'failure','错误':'fail','丢失':'missing','危险':'danger','报错':'error','漏洞':'bug','示例':'example','引用':'quote','引述':'cite'}
    };

    let state = { panel: null, button: null, init: false };

    // ===== Function =====
    function detectNesting(textarea) {
        const lines = textarea.value.substring(0, textarea.selectionStart).split('\n');
        for (let i = lines.length - 1; i >= 0; i--) {
            const line = lines[i].trim();
            if (!line) return 0;
            const match = line.match(/^(>+)(\s*\[!|\s+.*|\s*$)/);
            if (match) return match[1].length;
            if (!line.startsWith('>')) break;
        }
        return 0;
    }

    function insertCallout(type) {
        const textarea = document.querySelector('.d-editor-input');
        if (!textarea) return;
        
        const pos = textarea.selectionStart;
        const before = textarea.value.substring(0, pos);
        const after = textarea.value.substring(pos);
        const level = detectNesting(textarea) + 1;
        const prefix = '>'.repeat(level);
        const needNewline = before.length > 0 && !before.endsWith('\n');
        const text = `${prefix} [!${type}]\n${prefix} `;
        const final = needNewline ? '\n' + text : text;
        
        textarea.value = before + final + after;
        textarea.selectionStart = textarea.selectionEnd = pos + final.length;
        textarea.dispatchEvent(new Event('input', { bubbles: true }));
        textarea.focus();
        hidePanel();
    }

    function createButton() {
        const toolbar = document.querySelector('.d-editor-button-bar');
        if (!toolbar || toolbar.querySelector('.callout-btn')) return;
        
        const btn = document.createElement('button');
        btn.className = 'btn no-text btn-icon callout-btn';
        btn.title = 'Callout tool';
        btn.textContent = 'C';
        btn.style.cssText = 'width:28px;height:28px;border-radius:6px;background:white;border:none;margin-left:8px;cursor:pointer;color:black;font-weight:bold;font-size:14px;transition:all 0.2s ease';
        btn.onclick = e => { e.preventDefault(); e.stopPropagation(); togglePanel(); };
        toolbar.appendChild(btn);
        state.button = btn;
    }

    function createPanel() {
        const panel = document.createElement('div');
        panel.className = 'callout-panel';
        panel.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;border:1px solid #ddd;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.15);padding:16px;z-index:10000;display:none;max-width:600px;max-height:500px;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none';
        
        // 隐藏滚动条样式
        if (!document.querySelector('#callout-style')) {
            const style = document.createElement('style');
            style.id = 'callout-style';
            style.textContent = '.callout-panel::-webkit-scrollbar{display:none}';
            document.head.appendChild(style);
        }
        
        panel.innerHTML = `<div style="font-size:16px;font-weight:bold;margin-bottom:12px;text-align:center;color:#333;border-bottom:1px solid #eee;padding-bottom:8px">Callout 类型选择</div>
        <table style="width:100%;border-collapse:collapse;font-size:13px">
            <thead><tr style="background:#f5f5f5">
                <th style="padding:8px;border:1px solid #ddd;text-align:left">类型</th>
                <th style="padding:8px;border:1px solid #ddd;text-align:left">中文名</th>
                <th style="padding:8px;border:1px solid #ddd;text-align:center">快捷键</th>
                <th style="padding:8px;border:1px solid #ddd;text-align:center">颜色</th>
                <th style="padding:8px;border:1px solid #ddd;text-align:center">操作</th>
            </tr></thead><tbody></tbody></table>`;
        
        const tbody = panel.querySelector('tbody');
        config.types.forEach(([type, name, color, shortcut]) => {
            const row = document.createElement('tr');
            row.style.cssText = 'cursor:pointer;transition:background-color 0.2s ease';
            row.innerHTML = `<td style="padding:6px 8px;border:1px solid #ddd;font-family:monospace">${type}</td>
                <td style="padding:6px 8px;border:1px solid #ddd">${name}</td>
                <td style="padding:6px 8px;border:1px solid #ddd;text-align:center">${shortcut ? 'Ctrl+'+shortcut : '-'}</td>
                <td style="padding:6px 8px;border:1px solid #ddd;text-align:center"><span style="display:inline-block;width:16px;height:16px;border-radius:50%;background:${color};border:1px solid #ccc"></span></td>
                <td style="padding:6px 8px;border:1px solid #ddd;text-align:center"><button class="ins-btn" style="background:${color};color:white;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;font-size:11px">插入</button></td>`;
            
            row.onmouseenter = () => row.style.backgroundColor = '#f9f9f9';
            row.onmouseleave = () => row.style.backgroundColor = 'transparent';
            row.onclick = e => { if (!e.target.classList.contains('ins-btn')) insertCallout(type); };
            row.querySelector('.ins-btn').onclick = e => { e.stopPropagation(); insertCallout(type); };
            tbody.appendChild(row);
        });
        
        document.body.appendChild(panel);
        state.panel = panel;
    }

    function togglePanel() {
        if (!state.panel) createPanel();
        state.panel.style.display = state.panel.style.display === 'none' ? 'block' : 'none';
    }

    function hidePanel() {
        if (state.panel) state.panel.style.display = 'none';
    }

    // ===== Event handling =====
    function handleKeys(e) {
        if (e.key === 'Escape') return hidePanel();
        
        if (e.ctrlKey && !e.shiftKey && !e.altKey && e.target.tagName === 'TEXTAREA') {
            const type = config.shortcuts[e.key];
            if (type) { e.preventDefault(); insertCallout(type); }
            return;
        }
        
        if (e.key === 'Tab' && e.target.tagName === 'TEXTAREA') {
            const textarea = e.target;
            const text = textarea.value;
            const pos = textarea.selectionStart;
            const lines = text.substring(0, pos).split('\n');
            const line = lines[lines.length - 1];
            
            if (!line || line.trim().startsWith('> [!')) return;
            
            const prefix = line.trim().split(/[\s::]/)[0];
            const type = config.chinese[prefix];
            
            if (type) {
                e.preventDefault();
                const rest = line.substring(line.indexOf(prefix) + prefix.length).trim();
                const title = rest.startsWith(':') || rest.startsWith(':') ? rest.substring(1).trim() : rest;
                const newLine = `> [!${type}]${title ? ' ' + title : ''}`;
                lines[lines.length - 1] = newLine;
                const newText = lines.join('\n') + text.substring(pos);
                const lineStart = text.substring(0, pos).lastIndexOf('\n') + 1;
                
                textarea.value = newText;
                textarea.selectionStart = textarea.selectionEnd = lineStart + newLine.length;
                textarea.dispatchEvent(new Event('input', { bubbles: true }));
            }
        }
    }

    function handleClick(e) {
        if (state.panel && state.panel.style.display !== 'none' && 
            !state.panel.contains(e.target) && !e.target.classList.contains('callout-btn')) {
            hidePanel();
        }
    }

    // ===== Initialization =====
    function init() {
        if (state.init) return;
        document.addEventListener('keydown', handleKeys);
        document.addEventListener('click', handleClick);
        new MutationObserver(() => setTimeout(createButton, 100)).observe(document.body, {childList: true, subtree: true});
        createButton();
        state.init = true;
        console.log('Callout已初始化');
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
    window.addEventListener('load', () => setTimeout(init, 500));
})();

[!success]
功能:按钮+表格选择+嵌套+快捷键+文字Tab
解决:不想记or记不住or懒的打字的问题


34 个赞

[!done]+成功
方便呀佬 用上了

3 个赞

感谢分享

1 个赞

太强了佬感谢分享

1 个赞

[!success]
感谢佬友分享

[!done]

1 个赞

[!tip]非常棒
已经用了三四个脚本了

1 个赞

[!summary]
感谢佬,方便了!

1 个赞

太强了!

1 个赞

这个不错,给佬友点赞。

1 个赞

[!done]
完美

1 个赞

[!important]给佬友点赞,这个也很方便。

2 个赞

[!info]
效果不错啊

2 个赞

感谢大佬分享,社区有您更美丽

1 个赞

[!success]
很好用

[!abstract]-
用上了

[!note]
test

[!note]
用上了

[!success]
Done!

[!note]

[!abstract]

[!summary]

[!info]

[!tip]

[!hint]

[!important]

[!success]

[!done]

[!check]

[!question]

[!help]

[!faq]

[!warning]

[!caution]

[!attention]

[!failure]

[!fail]

[!missing]

[!danger]

[!error]

[!bug]

[!example]

[!quote]

[!cite]
测试,搞成叠叠乐了 :grinning_face:

1 个赞

测试发现最多叠12层还能看见 :face_savoring_food:

1 个赞