Obisidian Callouts脚本

原话题

油猴Obisidian Callouts脚本之二
// ==UserScript== // @name Markdown Callout // @namespace http://tampermonkey.net/ // @version 3.0 // @description Obsidian风格Calloutcha插件,支持输入中文关键词转换并附带工具栏按钮 // @match https://linux.do/* // @grant none // ==/UserScript==

(function() {
‘use strict’;

// 核心功能:定义中文别名到英文关键词的映射
const calloutAliasMap = {
    '笔记': 'note',
    '摘要': 'abstract', '概要': 'abstract', '总结': 'summary',
    '信息': 'info',
    '待办': 'todo', '任务': 'todo',
    '技巧': 'tip', '提示': 'tip', '窍门': 'hint',
    '重要': 'important',
    '成功': 'success', '完成': 'done', '检查': 'check',
    '问题': 'question', '帮助': 'help', '问答': 'faq',
    '警告': 'warning', '注意': 'caution', '当心': 'attention',
    '失败': 'failure', '错误': 'fail', '丢失': 'missing',
    '危险': 'danger', '报错': 'error', '漏洞': 'bug',
    '示例': 'example', '例子': 'example',
    '引用': 'quote', '引述': 'cite',
};

// Callout类型配置,包含颜色和中文名称
const calloutTypes = [
    { type: 'note', name: '笔记', color: '#448aff' },
    { type: 'abstract', name: '摘要', color: '#00b0ff' },
    { type: 'info', name: '信息', color: '#00b8d4' },
    { type: 'todo', name: '待办', color: '#00bcd4' },
    { type: 'tip', name: '技巧', color: '#00c853' },
    { type: 'success', name: '成功', color: '#00e676' },
    { type: 'question', name: '问题', color: '#64dd17' },
    { type: 'warning', name: '警告', color: '#ff9800' },
    { type: 'failure', name: '失败', color: '#ff5722' },
    { type: 'danger', name: '危险', color: '#f44336' },
    { type: 'bug', name: '漏洞', color: '#e91e63' },
    { type: 'example', name: '示例', color: '#7c4dff' },
    { type: 'quote', name: '引用', color: '#9e9e9e' }
];

/**
 * 获取当前编辑器状态的辅助函数
 */
function getEditorState(target) {
    const text = target.value;
    const selectionStart = target.selectionStart;
    const textUpToCursor = text.substring(0, selectionStart);
    const currentLineIndex = textUpToCursor.split('\n').length - 1;
    const lines = text.split('\n');
    return {
        lines: lines,
        currentLineIndex: currentLineIndex,
        currentLine: lines[currentLineIndex],
        lineStartIndex: textUpToCursor.lastIndexOf('\n') + 1,
    };
}

/**
 * 检测当前光标位置的Callout嵌套层级
 */
function detectCalloutNesting(textarea) {
    const text = textarea.value;
    const cursorPos = textarea.selectionStart;
    const textBeforeCursor = text.substring(0, cursorPos);
    const lines = textBeforeCursor.split('\n');

    // 从当前行开始往上查找,找到最近的Callout上下文
    for (let i = lines.length - 1; i >= 0; i--) {
        const line = lines[i];
        const trimmedLine = line.trim();

        // 如果遇到完全空行,说明嵌套被中断,重置为0
        if (trimmedLine === '') {
            return 0;
        }

        // 检查是否是Callout相关的行(标题行、内容行或空的>行)
        const calloutMatch = trimmedLine.match(/^(>+)(\s*\[!|\s+.*|\s*$)/);
        if (calloutMatch) {
            return calloutMatch[1].length;
        }

        // 如果遇到非Callout行,说明不在Callout上下文中
        break;
    }

    return 0;
}

/**
 * 插入Callout样式到编辑器
 */
function insertCallout(textarea, calloutType, title = '') {
    const selectionStart = textarea.selectionStart;
    const selectionEnd = textarea.selectionEnd;
    const selectedText = textarea.value.substring(selectionStart, selectionEnd);

    if (selectedText) {
        // 如果有选中文本,只在选中内容前面添加一个Callout样式
        const calloutHeader = `> [!${calloutType}]${title ? ' ' + title : ''}\n> `;

        const beforeText = textarea.value.substring(0, selectionStart);
        const afterText = textarea.value.substring(selectionEnd);

        // 检查是否需要在前面添加换行
        const textBeforeCursor = textarea.value.substring(0, selectionStart);
        const currentLineStart = textBeforeCursor.lastIndexOf('\n') + 1;
        const currentLine = textBeforeCursor.substring(currentLineStart);

        // 为选中文本的Callout样式前面总是添加一个换行
        let finalCalloutText = '\n' + calloutHeader;
        if (currentLine.trim() !== '' && !textBeforeCursor.endsWith('\n')) {
            finalCalloutText = '\n' + finalCalloutText;
        }

        textarea.value = beforeText + finalCalloutText + selectedText + afterText;

        // 设置光标位置到选中文本的末尾
        const newCursorPos = selectionStart + finalCalloutText.length + selectedText.length;
        textarea.selectionStart = textarea.selectionEnd = newCursorPos;
    } else {
        // 如果没有选中文本,使用原来的嵌套逻辑
        // 检测当前的嵌套层级
        const currentNestingLevel = detectCalloutNesting(textarea);

        // 每次点击都增加一层嵌套
        const nestingLevel = currentNestingLevel + 1;
        const prefix = '>'.repeat(nestingLevel);

        const calloutText = `${prefix} [!${calloutType}]${title ? ' ' + title : ''}\n${prefix} `;

        // 检查是否需要在前面添加换行
        const textBeforeCursor = textarea.value.substring(0, selectionStart);
        const currentLineStart = textBeforeCursor.lastIndexOf('\n') + 1;
        const currentLine = textBeforeCursor.substring(currentLineStart);

        let finalCalloutText = calloutText;
        if (currentLine.trim() !== '' && !textBeforeCursor.endsWith('\n')) {
            finalCalloutText = '\n' + calloutText;
        }

        const beforeText = textarea.value.substring(0, selectionStart);
        const afterText = textarea.value.substring(selectionEnd);

        textarea.value = beforeText + finalCalloutText + afterText;

        // 设置光标位置到内容区域末尾
        const newCursorPos = selectionStart + finalCalloutText.length;
        textarea.selectionStart = textarea.selectionEnd = newCursorPos;
    }

    // 触发input事件
    textarea.dispatchEvent(new Event('input', { bubbles: true }));
    textarea.focus();
}

/**
 * 更新编辑器内容和光标位置的辅助函数
 */
function updateEditor(target, lines, lineIndex, newLine, lineStartIndex) {
    lines[lineIndex] = newLine;
    const newText = lines.join('\n');
    target.value = newText;
    target.selectionStart = target.selectionEnd = lineStartIndex + newLine.length;

    // 触发 input 事件以确保页面能监听到变化
    target.dispatchEvent(new Event('input', { bubbles: true }));
}

/**
 * 创建工具栏按钮
 */
function createToolbarButtons() {
    // 查找编辑器工具栏
    const toolbar = document.querySelector('.d-editor-button-bar');
    if (!toolbar) return;

    // 检查是否已经添加过按钮
    if (toolbar.querySelector('.callout-buttons-container')) return;

    // 创建按钮容器
    const container = document.createElement('div');
    container.className = 'callout-buttons-container';
    container.style.cssText = `
        display: inline-flex;
        gap: 4px;
        margin-left: 8px;
        align-items: center;
    `;

    // 为每种callout类型创建按钮
    calloutTypes.forEach(callout => {
        const button = document.createElement('button');
        button.className = 'btn no-text btn-icon callout-btn';
        button.title = `插入${callout.name} Callout`;
        button.style.cssText = `
            width: 20px;
            height: 20px;
            border-radius: 50%;
            background-color: ${callout.color};
            border: 2px solid #fff;
            box-shadow: 0 1px 3px rgba(0,0,0,0.3);
            margin: 0 1px;
            cursor: pointer;
            transition: transform 0.1s ease;
        `;

        button.addEventListener('mouseenter', () => {
            button.style.transform = 'scale(1.1)';
        });

        button.addEventListener('mouseleave', () => {
            button.style.transform = 'scale(1)';
        });

        button.addEventListener('click', (e) => {
            e.preventDefault();
            const textarea = document.querySelector('.d-editor-input');
            if (textarea) {
                insertCallout(textarea, callout.type);
            }
        });

        container.appendChild(button);
    });

    toolbar.appendChild(container);
}



/**
 * 主事件处理函数
 * @param {KeyboardEvent} event
 */
function handleTabPress(event) {
    if (event.key !== 'Tab' || event.target.tagName !== 'TEXTAREA') {
        return;
    }

    const target = event.target;
    const { lines, currentLineIndex, currentLine, lineStartIndex } = getEditorState(target);

    // 如果当前行无效或已经是 Callout,则不执行任何操作
    if (currentLine === undefined || currentLine.trim().startsWith('> [!')) {
        return;
    }

    // 阻止 Tab 的默认行为(如切换焦点)
    event.preventDefault();

    const prefix = currentLine.trim().split(/[\s::]/)[0]; // 支持中文冒号
    const targetKeyword = calloutAliasMap[prefix];

    // 如果找到了匹配的中文关键词
    if (targetKeyword) {
        const restOfLine = currentLine.substring(currentLine.indexOf(prefix) + prefix.length).trim();
        const title = restOfLine.startsWith(':') || restOfLine.startsWith(':')
            ? restOfLine.substring(1).trim()
            : restOfLine;

        const newLine = `> [!${targetKeyword}]${title ? ' ' + title : ''}`;
        updateEditor(target, lines, currentLineIndex, newLine, lineStartIndex);
    }
}

/**
 * 初始化函数
 */
function init() {
    // 添加工具栏按钮
    createToolbarButtons();

    // 监听DOM变化,以便在新的编辑器出现时添加按钮
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length > 0) {
                // 延迟执行,确保DOM完全加载
                setTimeout(createToolbarButtons, 100);
            }
        });
    });

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

// 事件监听器
document.addEventListener('keydown', handleTabPress);

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

// 也在页面完全加载后再次尝试初始化
window.addEventListener('load', () => {
    setTimeout(init, 500);
});

})();

这个脚本是从 搞一点油猴的 Obsidian Callouts - 开发调优 - LINUX DO修改来的
在原有功能上新增了颜色工具栏,点击即可添加样式,支持嵌套

image

另外也支持选中内容后点击颜色按钮来添加,会将选中内容移动到新的嵌套

image