原文见感谢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懒的打字的问题


