可视化的油猴的 Obsidian Callouts
- 内容介绍
- 文章标签
- 相关推荐
基于佬友的脚本做了点微小的改进,同时支持输入中文别名,按下Tab键完成转换
搞一点油猴的 Obsidian Callouts 开发调优油猴脚本 安装脚本并刷新页面后,输入中文别名,按下Tab键完成转换并可预览效果 [20250919-0147-59.0972439] 中文别名 callouts 效果(描述) 笔记 note 蓝色信息笔记,通常带有灯泡或笔记图标,用于一般备注。 摘要 abstract 青色摘要框,用于总结或概要内容。 概要 abstract 青色摘要框,用于总结或概要内容。 总…
先看演示
最近更新:
支持可视化的油猴的 Obsidian Callouts - #62,来自 Jenhy 功能实现
支持idc flare里使用
主要改进点
-
中英符号唤起,支持输入>或者》后,再按 Tab 唤起
-
可视化展示,icon、颜色预览,无需输入完成后才能看到效果
image273×274 11.5 KB -
快速检索,中英支持
image292×191 7.63 KB
image287×126 4.16 KB -
自动层级嵌套(不建议嵌套的花里胡哨,影响佬友看帖)
image1469×519 18.9 KB
给记不住关键词的佬友享用,复制脚本粘贴使用即可
芝麻开门
// ==UserScript==
// @name Markdown Callout
// @namespace http://tampermonkey.net/
// @version 3.1
// @description 》或 > + Tab 唤起;去重;图标预览
// @match https://linux.do/*
// @match https://idcflare.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const TRIGGERS = ['》', '>'];
const Z_INDEX = 2147483647;
const MENU_WIDTH = 240;
const MENU_MAX_H = 220;
const calloutAliasMap = {
'笔记': 'note',
'摘要': 'abstract', '概要': 'abstract', '总结': 'summary',
'信息': 'info',
'待办': 'todo', '任务': 'todo',
'技巧': 'tip', '提示': 'tip', '窍门': 'hint',
'重要': 'important',
'成功': 'success', '完成': 'done', '检查': 'check',
'问题': 'question', '帮助': 'help', '问答': 'faq',
'警告': 'warning', '注意': 'caution', '当心': 'attention',
'失败': 'failure', '错误': 'error', '丢失': 'missing', '漏洞': 'bug', 'bug': 'bug',
'危险': 'danger',
'示例': 'example', '例子': 'example',
'引用': 'quote', '引述': 'cite',
};
const calloutColors = {
note:'#64748b', abstract:'#8b5cf6', summary:'#8b5cf6',
info:'#0ea5e9', help:'#0ea5e9', faq:'#0ea5e9', question:'#06b6d4',
tip:'#10b981', hint:'#14b8a6', important:'#fb7185',
success:'#22c55e', done:'#22c55e', check:'#22c55e',
warning:'#f59e0b', caution:'#f59e0b', attention:'#f59e0b',
failure:'#ef4444', error:'#ef4444', missing:'#ef4444', danger:'#ef4444', bug:'#f97316',
example:'#a78bfa', quote:'#94a3b8', cite:'#94a3b8',
todo:'#60a5fa',
};
/*** 工具函数 ***/
const INVIS_ALL=/[\u200B-\u200D\uFEFF\u00A0]/g;
const INVIS_OR_SPACE=/[\u200B-\u200D\uFEFF\u00A0\s]/; // ← 加入空格
function cleanInvisibles(s){ return s.replace(INVIS_ALL,''); }
function findTriggerBeforeCaret(value, lineStartIndex, caret){
for(let i=caret-1; i>=lineStartIndex; i--){
const c=value[i];
if(INVIS_OR_SPACE.test(c)) continue;
return TRIGGERS.includes(c) ? i : -1;
}
return -1;
}
const clamp=(n,min,max)=>Math.min(max,Math.max(min,n));
function hexToRGBA(hex,a=0.16){
const h=hex.replace('#',''); const to=v=>parseInt(v,16);
const r=h.length===3? to(h[0]+h[0]) : to(h.slice(0,2));
const g=h.length===3? to(h[1]+h[1]) : to(h.slice(2,4));
const b=h.length===3? to(h[2]+h[2]) : to(h.slice(4,6));
return `rgba(${r},${g},${b},${a})`;
}
function getEditorState(target){
const text=target.value, selectionStart=target.selectionStart??0, up=text.substring(0,selectionStart);
const currentLineIndex=up.split('\n').length-1; const lines=text.split('\n');
return { text, lines, selectionStart, currentLineIndex, currentLine:lines[currentLineIndex]??'', lineStartIndex:up.lastIndexOf('\n')+1 };
}
function updateEditor(target,lines,lineIndex,newLine,lineStartIndex){
lines[lineIndex]=newLine; const newText=lines.join('\n'); target.value=newText;
const caret=lineStartIndex+newLine.length; target.selectionStart=target.selectionEnd=caret;
target.dispatchEvent(new Event('input',{bubbles:true}));
}
function getPrevCalloutDepth(lines,idx){
const i=idx-1; if(i<0) return 0;
const line=(lines[i]??''); if(line.trim()==='') return 0;
const m=line.match(/^\s*(>+)\s*\[\!/); return m?m[1].length:0;
}
function getCaretPagePosition(textarea,caretIndex){
const style=window.getComputedStyle(textarea);
const mirror=document.createElement('div');
['direction','boxSizing','width','height','overflowX','overflowY',
'borderTopWidth','borderRightWidth','borderBottomWidth','borderLeftWidth',
'paddingTop','paddingRight','paddingBottom','paddingLeft',
'fontStyle','fontVariant','fontWeight','fontStretch','fontSize','fontFamily',
'lineHeight','textAlign','textTransform','textIndent','textDecoration',
'letterSpacing','wordSpacing','tabSize','MozTabSize'
].forEach(p=>mirror.style[p]=style[p]);
mirror.style.whiteSpace='pre-wrap'; mirror.style.wordWrap='break-word';
mirror.style.position='fixed'; mirror.style.visibility='hidden';
const taRect=textarea.getBoundingClientRect(); mirror.style.left=taRect.left+'px'; mirror.style.top=taRect.top+'px';
const value=textarea.value; const pre=value.slice(0,caretIndex); const post=value.slice(caretIndex)||'.';
const span=document.createElement('span'); span.textContent=post[0]; mirror.textContent=pre; mirror.appendChild(span);
(document.getElementById('reply-control')||document.body).appendChild(mirror);
const rect=span.getBoundingClientRect(); const x=rect.left, y=rect.bottom; mirror.remove(); return {x,y};
}
const aliasToKeyword={...calloutAliasMap};
Object.values(calloutAliasMap).forEach(k=>aliasToKeyword[k]=k);
const keywordToAliases={}; Object.entries(calloutAliasMap).forEach(([a,k])=>{ (keywordToAliases[k] ||= []).push(a); });
function pickDisplayAliasForKeyword(k){ return (keywordToAliases[k]&&keywordToAliases[k][0])||k; }
const uniqueKeywords=[...new Set(Object.values(aliasToKeyword))];
const baseItems=uniqueKeywords.map(k=>({display:pickDisplayAliasForKeyword(k), keyword:k}));
function filterItems(input){
const q=input.trim(); if(!q) return baseItems.slice();
const lower=q.toLowerCase(); const matched=new Set();
Object.keys(aliasToKeyword).forEach(a=>{ if(a.includes(q)||a.toLowerCase().includes(lower)) matched.add(aliasToKeyword[a]); });
uniqueKeywords.forEach(k=>{ if(k.includes(q)||k.toLowerCase().includes(lower)) matched.add(k); });
const list=[...matched].map(k=>({display:pickDisplayAliasForKeyword(k), keyword:k}));
list.sort((a,b)=>{
const ap=(a.display.startsWith(q)||a.keyword.startsWith(q))?0:1;
const bp=(b.display.startsWith(q)||b.keyword.startsWith(q))?0:1;
return ap-bp||a.display.localeCompare(b.display,'zh');
}); return list;
}
function makeIcon(kw,color){
const NS='http://www.w3.org/2000/svg';
const svg=document.createElementNS(NS,'svg'); svg.setAttribute('width','16'); svg.setAttribute('height','16'); svg.setAttribute('viewBox','0 0 24 24'); svg.setAttribute('fill','none'); svg.style.flex='0 0 auto';
const p=document.createElementNS(NS,'path'); p.setAttribute('stroke',color||'#9aa4b2'); p.setAttribute('stroke-width','2'); p.setAttribute('stroke-linecap','round'); p.setAttribute('stroke-linejoin','round');
const k=kw.toLowerCase();
if(['info','help','faq','summary','abstract','note'].some(s=>k.includes(s))) p.setAttribute('d','M12 8h.01M12 12v4m0 6a10 10 0 100-20 10 10 0 000 20z');
else if(['warning','caution','attention'].some(s=>k.includes(s))) p.setAttribute('d','M12 9v4m0 4h.01M10.29 3.86l-8.48 14.7A2 2 0 003.52 22h16.96a2 2 0 001.71-3.44L13.71 3.86a2 2 0 00-3.42 0z');
else if(['success','done','check'].some(s=>k.includes(s))) p.setAttribute('d','M9 12l2 2 4-4m7 2a9 9 0 11-18 0 9 9 0 0118 0z');
else if(['tip','hint','important'].some(s=>k.includes(s))) p.setAttribute('d','M12 2a7 7 0 00-7 7c0 2.76 1.67 5.14 4.06 6.21L9 19h6l-.06-3.79A7.002 7.002 0 0012 2zM9 22h6');
else if(['danger','failure','error','missing'].some(s=>k.includes(s))) p.setAttribute('d','M10 10l4 4m0-4l-4 4M12 22a10 10 0 100-20 10 10 0 000 20z');
else if(['question'].some(s=>k.includes(s))) p.setAttribute('d','M9 9a3 3 0 116 0c0 2-3 2-3 4m0 4h.01M12 22a10 10 0 100-20 10 10 0 000 20z');
else if(['example'].some(s=>k.includes(s))) p.setAttribute('d','M4 7h16M4 12h16M4 17h10');
else if(['quote','cite'].some(s=>k.includes(s))) p.setAttribute('d','M7 7h5v5H9a4 4 0 00-4 4v1M17 7h5v5h-3a4 4 0 00-4 4v1');
else if(['todo'].some(s=>k.includes(s))) p.setAttribute('d','M9 11l3 3L22 4M3 5h6M3 10h6M3 15h6M3 20h6');
else if(['bug'].some(s=>k.includes(s))) p.setAttribute('d','M14 7h-4a4 4 0 00-4 4v2a4 4 0 004 4h4a4 4 0 004-4v-2a4 4 0 00-4-4zM6 7l-2-2M18 7l2-2M6 17l-2 2M18 17l2 2');
else p.setAttribute('d','M12 8h.01M12 12v4m0 6a10 10 0 100-20 10 10 0 000 20z');
svg.appendChild(p); return svg;
}
let menuEl=null, menuVisible=false, activeIdx=-1, filteredItems=[];
let anchorTextarea=null, anchorTriggerIndex=-1, anchorLineIndex=-1;
function ensureBaseStyle(){
if(document.getElementById('callout-suggest-style')) return;
const st=document.createElement('style'); st.id='callout-suggest-style';
st.textContent=`
#callout-suggest-menu{
position:fixed; z-index:${Z_INDEX}; display:none; user-select:none;
border-radius:10px; border:1px solid rgba(255,255,255,0.16);
font-size:13px; line-height:1.6; padding:6px 0;
min-width:${MENU_WIDTH}px;
max-height:${MENU_MAX_H}px;
box-sizing:border-box;
overflow-y:auto; overflow-x:hidden;
backdrop-filter: blur(8px);
--fg:#e6edf3;
--bg0: rgba(30,41,59,0.88);
--bg1: rgba(17,24,39,0.88);
--scrollbar: rgba(148,163,184,0.45);
--scrollbar-hover: rgba(148,163,184,0.90);
--item-hover: rgba(255,255,255,0.06);
background: linear-gradient(180deg, var(--bg0) 0%, var(--bg1) 100%) !important;
color: var(--fg) !important;
color-scheme: dark;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar) transparent;
}
#callout-suggest-menu, #callout-suggest-menu * { color: var(--fg) !important; }
#callout-suggest-menu::-webkit-scrollbar{ width:8px; }
#callout-suggest-menu::-webkit-scrollbar-track{ background:transparent; }
#callout-suggest-menu::-webkit-scrollbar-thumb{ background-color:var(--scrollbar); border-radius:6px; }
#callout-suggest-menu:hover::-webkit-scrollbar-thumb{ background-color:var(--scrollbar-hover); }
#callout-suggest-menu .item{ padding:6px 10px; cursor:pointer; display:flex; align-items:center; gap:8px; }
#callout-suggest-menu .item:hover{ background: var(--item-hover) !important; }
#callout-suggest-menu .item > div{ flex:1 1 auto; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
`;
document.head.appendChild(st);
}
function ensureMenu(){
ensureBaseStyle();
if(menuEl) return menuEl;
const m=document.createElement('div'); m.id='callout-suggest-menu';
(document.getElementById('reply-control')||document.body).appendChild(m);
m.style.minWidth = MENU_WIDTH + 'px';
m.style.maxHeight = MENU_MAX_H + 'px';
m.style.overflowY = 'auto';
m.style.overflowX = 'hidden';
m.style.boxSizing = 'border-box';
menuEl=m; return m;
}
function applyActiveStyle(el,active){
const kw=el.dataset.keyword||''; const color=calloutColors[kw]||'#9aa4b2';
if(active){ el.style.background=hexToRGBA(color,0.18); el.style.borderLeft=`4px solid ${color}`; el.style.borderRadius='6px'; }
else { el.style.background=''; el.style.borderLeft='0'; }
}
function renderMenu(items){
ensureMenu(); menuEl.innerHTML='';
items.forEach((it,idx)=>{
const item=document.createElement('div'); item.className='item'; item.setAttribute('role','option');
item.dataset.index=String(idx); item.dataset.keyword=it.keyword;
const icon=makeIcon(it.keyword, calloutColors[it.keyword]||'#9aa4b2');
const text=document.createElement('div'); text.textContent=`${it.display} → ${it.keyword}`;
item.appendChild(icon); item.appendChild(text);
if(idx===activeIdx) applyActiveStyle(item,true);
item.addEventListener('mouseenter',()=> setActiveIdx(idx));
item.addEventListener('mousedown',e=>e.preventDefault());
item.addEventListener('click',()=> pickByKeyword(it.keyword));
menuEl.appendChild(item);
});
ensureActiveInView();
}
function ensureActiveInView(){
if(!menuVisible||!menuEl||activeIdx<0) return;
const node=menuEl.children[activeIdx]; if(!node||!node.scrollIntoView) return;
node.scrollIntoView({block:'nearest'});
}
function setActiveIdx(idx){
if(!filteredItems.length){ activeIdx=-1; return; }
if(idx<0) idx=0; if(idx>=filteredItems.length) idx=filteredItems.length-1;
activeIdx=idx; [...menuEl.children].forEach((el,i)=>applyActiveStyle(el,i===activeIdx)); ensureActiveInView();
}
function placeMenuAtViewportXY(x,y){
const margin=8; menuEl.style.display='block';
let left=Math.min(Math.max(margin,x),window.innerWidth-menuEl.offsetWidth-margin);
let top=y+4; const h=menuEl.offsetHeight;
if(top+h+margin>window.innerHeight) top=Math.max(margin,y-h-4);
menuEl.style.left=left+'px'; menuEl.style.top=top+'px';
}
function repositionMenu(){
if(!menuVisible||!anchorTextarea) return;
const {selectionStart}=anchorTextarea; const pos=getCaretPagePosition(anchorTextarea,selectionStart); placeMenuAtViewportXY(pos.x,pos.y);
}
/*** 打开/关闭 ***/
function openMenuAt(textarea,caretIndex,initialList){
ensureMenu(); filteredItems=initialList.slice(); activeIdx=filteredItems.length?0:-1;
renderMenu(filteredItems);
const pos=getCaretPagePosition(textarea,caretIndex); placeMenuAtViewportXY(pos.x,pos.y);
menuVisible=true; anchorTextarea=textarea;
textarea.focus(); // 防止任何意外失焦
}
function closeMenu(){
if(!menuEl) return; menuEl.style.display='none'; menuVisible=false;
filteredItems=[]; activeIdx=-1; anchorTextarea=null; anchorTriggerIndex=-1; anchorLineIndex=-1;
}
function buildCalloutLine(lines,idx,leadingSpaces,kw,title){
const prev=getPrevCalloutDepth(lines,idx); const depth=Math.max(1,prev+1);
return `${' '.repeat(leadingSpaces)}${'>'.repeat(depth)} [!${kw}]${title?' '+title:''}`;
}
function pickByKeyword(kw){
if(!anchorTextarea) return;
const ta = anchorTextarea;
const { text, lines, currentLineIndex, currentLine, lineStartIndex } = getEditorState(ta);
const relStart = Math.max(0, anchorTriggerIndex - lineStartIndex);
const caret = ta.selectionStart ?? (relStart + 1);
const prevDepth = getPrevCalloutDepth(lines, currentLineIndex);
const depth = Math.max(1, prevDepth + 1);
const leadingSpaces = (currentLine || '').length - (currentLine || '').trimStart().length;
const calloutLine = `${' '.repeat(leadingSpaces)}${'>'.repeat(depth)} [!${kw}]`;
const beforeTriggerInLine = currentLine.slice(0, relStart);
const afterCaretInLine = currentLine.slice(Math.max(0, caret - lineStartIndex));
const keptCurrentLine = beforeTriggerInLine + afterCaretInLine;
const textBeforeLine = text.slice(0, lineStartIndex);
const textAfterLine = text.slice(lineStartIndex + currentLine.length);
const newText = textBeforeLine + calloutLine + ' ' + keptCurrentLine + textAfterLine;
ta.value = newText;
const newCaret = textBeforeLine.length + calloutLine.length;
ta.selectionStart = ta.selectionEnd = newCaret;
ta.dispatchEvent(new Event('input', { bubbles: true }));
closeMenu();
ta.focus();
}
function onKeyDown(event){
const target=event.target; if(!(target instanceof HTMLTextAreaElement)) return;
if(event.key==='Tab'){
const st=getEditorState(target);
const triggerIdx = findTriggerBeforeCaret(target.value, st.lineStartIndex, st.selectionStart);
if(triggerIdx !== -1){
event.preventDefault(); event.stopPropagation(); if(event.stopImmediatePropagation) event.stopImmediatePropagation();
anchorTriggerIndex=triggerIdx; anchorLineIndex=st.currentLineIndex;
const upto=cleanInvisibles(target.value.slice(triggerIdx+1,st.selectionStart)).trimStart();
openMenuAt(target,st.selectionStart,filterItems(upto));
return;
}
const line=st.currentLine; const prefix=line.trim().split(/[\s::]/)[0]; const kw=aliasToKeyword[prefix];
if(kw){
event.preventDefault(); event.stopPropagation(); if(event.stopImmediatePropagation) event.stopImmediatePropagation();
const lead=line.length-line.trimStart().length;
const rest=line.trim().substring(prefix.length).trim().replace(/^[::]\s*/,'');
const newLine=buildCalloutLine(st.lines,st.currentLineIndex,lead,kw,rest);
updateEditor(target,st.lines,st.currentLineIndex,newLine,st.lineStartIndex);
target.focus();
return;
}
}
if(menuVisible){
if(event.key==='Escape'){ event.preventDefault(); closeMenu(); return; }
if(event.key==='ArrowDown'){ event.preventDefault(); setActiveIdx(activeIdx+1); return; }
if(event.key==='ArrowUp'){ event.preventDefault(); setActiveIdx(activeIdx-1); return; }
if(event.key==='Enter'){ if(activeIdx>=0){ event.preventDefault(); pickByKeyword(filteredItems[activeIdx].keyword); } return; }
if(event.key==='Tab'){ event.preventDefault(); return; }
}
}
function onInput(event){
if(!menuVisible||!anchorTextarea) return;
const target=event.target; if(target!==anchorTextarea) return;
const {selectionStart,currentLineIndex,lineStartIndex}=getEditorState(target);
if(currentLineIndex!==anchorLineIndex){ closeMenu(); return; }
if(anchorTriggerIndex<lineStartIndex || anchorTriggerIndex>=target.value.length){ closeMenu(); return; }
const between=target.value.slice(anchorTriggerIndex+1,selectionStart);
if(between.includes('\n')){ closeMenu(); return; }
// 再次确认触发符仍在
if(!TRIGGERS.includes(target.value[anchorTriggerIndex])){ closeMenu(); return; }
const q=cleanInvisibles(between).trim();
const next=filterItems(q); filteredItems=next.length?next:baseItems.slice();
activeIdx=filteredItems.length?0:-1; renderMenu(filteredItems);
const pos=getCaretPagePosition(target,selectionStart); placeMenuAtViewportXY(pos.x,pos.y);
target.focus(); // 保持焦点
}
function onDocMouseDown(e){
if(!menuVisible) return;
if(menuEl && !menuEl.contains(e.target)){
if(!(e.target instanceof HTMLTextAreaElement)) closeMenu();
}
}
document.addEventListener('keydown',onKeyDown,true);
document.addEventListener('input',onInput,true);
document.addEventListener('mousedown',onDocMouseDown,true);
})();
--【壹】--:
[!hint]好耶好耶!
--【贰】--:
[!success]真的很强
--【叁】--:
太强了,大佬
--【肆】--:
[!success]
效果很好!
--【伍】--:
[!note]
好用!
--【陆】--:
[!todo] 加一个IF.站的 测试了OK的
// @match https://idcflare.com/*
--【柒】--:
感谢佬分享!!
--【捌】--:
[!quote] 太强了佬
这下不得不支持了
--【玖】--:
爱站那边也可以用这个脚本哈???
--【拾】--:
[!abstract] 哦哟
哦哟
--【拾壹】--:
喜欢就好啊
--【拾贰】--:
给力,感谢分享
--【拾叁】--:
[!hint] 马上使用 很强大 感谢分享
--【拾肆】--:
[!info]
的确方便
--【拾伍】--:
[!success]-已用上,测试一下!
--【拾陆】--:
感谢佬友分享
--【拾柒】--:
[!tip]太牛了~这个更方便了
--【拾捌】--:
很不错,感谢佬友的分享。
--【拾玖】--:
[!hint]不错不错
基于佬友的脚本做了点微小的改进,同时支持输入中文别名,按下Tab键完成转换
搞一点油猴的 Obsidian Callouts 开发调优油猴脚本 安装脚本并刷新页面后,输入中文别名,按下Tab键完成转换并可预览效果 [20250919-0147-59.0972439] 中文别名 callouts 效果(描述) 笔记 note 蓝色信息笔记,通常带有灯泡或笔记图标,用于一般备注。 摘要 abstract 青色摘要框,用于总结或概要内容。 概要 abstract 青色摘要框,用于总结或概要内容。 总…
先看演示
最近更新:
支持可视化的油猴的 Obsidian Callouts - #62,来自 Jenhy 功能实现
支持idc flare里使用
主要改进点
-
中英符号唤起,支持输入>或者》后,再按 Tab 唤起
-
可视化展示,icon、颜色预览,无需输入完成后才能看到效果
image273×274 11.5 KB -
快速检索,中英支持
image292×191 7.63 KB
image287×126 4.16 KB -
自动层级嵌套(不建议嵌套的花里胡哨,影响佬友看帖)
image1469×519 18.9 KB
给记不住关键词的佬友享用,复制脚本粘贴使用即可
芝麻开门
// ==UserScript==
// @name Markdown Callout
// @namespace http://tampermonkey.net/
// @version 3.1
// @description 》或 > + Tab 唤起;去重;图标预览
// @match https://linux.do/*
// @match https://idcflare.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const TRIGGERS = ['》', '>'];
const Z_INDEX = 2147483647;
const MENU_WIDTH = 240;
const MENU_MAX_H = 220;
const calloutAliasMap = {
'笔记': 'note',
'摘要': 'abstract', '概要': 'abstract', '总结': 'summary',
'信息': 'info',
'待办': 'todo', '任务': 'todo',
'技巧': 'tip', '提示': 'tip', '窍门': 'hint',
'重要': 'important',
'成功': 'success', '完成': 'done', '检查': 'check',
'问题': 'question', '帮助': 'help', '问答': 'faq',
'警告': 'warning', '注意': 'caution', '当心': 'attention',
'失败': 'failure', '错误': 'error', '丢失': 'missing', '漏洞': 'bug', 'bug': 'bug',
'危险': 'danger',
'示例': 'example', '例子': 'example',
'引用': 'quote', '引述': 'cite',
};
const calloutColors = {
note:'#64748b', abstract:'#8b5cf6', summary:'#8b5cf6',
info:'#0ea5e9', help:'#0ea5e9', faq:'#0ea5e9', question:'#06b6d4',
tip:'#10b981', hint:'#14b8a6', important:'#fb7185',
success:'#22c55e', done:'#22c55e', check:'#22c55e',
warning:'#f59e0b', caution:'#f59e0b', attention:'#f59e0b',
failure:'#ef4444', error:'#ef4444', missing:'#ef4444', danger:'#ef4444', bug:'#f97316',
example:'#a78bfa', quote:'#94a3b8', cite:'#94a3b8',
todo:'#60a5fa',
};
/*** 工具函数 ***/
const INVIS_ALL=/[\u200B-\u200D\uFEFF\u00A0]/g;
const INVIS_OR_SPACE=/[\u200B-\u200D\uFEFF\u00A0\s]/; // ← 加入空格
function cleanInvisibles(s){ return s.replace(INVIS_ALL,''); }
function findTriggerBeforeCaret(value, lineStartIndex, caret){
for(let i=caret-1; i>=lineStartIndex; i--){
const c=value[i];
if(INVIS_OR_SPACE.test(c)) continue;
return TRIGGERS.includes(c) ? i : -1;
}
return -1;
}
const clamp=(n,min,max)=>Math.min(max,Math.max(min,n));
function hexToRGBA(hex,a=0.16){
const h=hex.replace('#',''); const to=v=>parseInt(v,16);
const r=h.length===3? to(h[0]+h[0]) : to(h.slice(0,2));
const g=h.length===3? to(h[1]+h[1]) : to(h.slice(2,4));
const b=h.length===3? to(h[2]+h[2]) : to(h.slice(4,6));
return `rgba(${r},${g},${b},${a})`;
}
function getEditorState(target){
const text=target.value, selectionStart=target.selectionStart??0, up=text.substring(0,selectionStart);
const currentLineIndex=up.split('\n').length-1; const lines=text.split('\n');
return { text, lines, selectionStart, currentLineIndex, currentLine:lines[currentLineIndex]??'', lineStartIndex:up.lastIndexOf('\n')+1 };
}
function updateEditor(target,lines,lineIndex,newLine,lineStartIndex){
lines[lineIndex]=newLine; const newText=lines.join('\n'); target.value=newText;
const caret=lineStartIndex+newLine.length; target.selectionStart=target.selectionEnd=caret;
target.dispatchEvent(new Event('input',{bubbles:true}));
}
function getPrevCalloutDepth(lines,idx){
const i=idx-1; if(i<0) return 0;
const line=(lines[i]??''); if(line.trim()==='') return 0;
const m=line.match(/^\s*(>+)\s*\[\!/); return m?m[1].length:0;
}
function getCaretPagePosition(textarea,caretIndex){
const style=window.getComputedStyle(textarea);
const mirror=document.createElement('div');
['direction','boxSizing','width','height','overflowX','overflowY',
'borderTopWidth','borderRightWidth','borderBottomWidth','borderLeftWidth',
'paddingTop','paddingRight','paddingBottom','paddingLeft',
'fontStyle','fontVariant','fontWeight','fontStretch','fontSize','fontFamily',
'lineHeight','textAlign','textTransform','textIndent','textDecoration',
'letterSpacing','wordSpacing','tabSize','MozTabSize'
].forEach(p=>mirror.style[p]=style[p]);
mirror.style.whiteSpace='pre-wrap'; mirror.style.wordWrap='break-word';
mirror.style.position='fixed'; mirror.style.visibility='hidden';
const taRect=textarea.getBoundingClientRect(); mirror.style.left=taRect.left+'px'; mirror.style.top=taRect.top+'px';
const value=textarea.value; const pre=value.slice(0,caretIndex); const post=value.slice(caretIndex)||'.';
const span=document.createElement('span'); span.textContent=post[0]; mirror.textContent=pre; mirror.appendChild(span);
(document.getElementById('reply-control')||document.body).appendChild(mirror);
const rect=span.getBoundingClientRect(); const x=rect.left, y=rect.bottom; mirror.remove(); return {x,y};
}
const aliasToKeyword={...calloutAliasMap};
Object.values(calloutAliasMap).forEach(k=>aliasToKeyword[k]=k);
const keywordToAliases={}; Object.entries(calloutAliasMap).forEach(([a,k])=>{ (keywordToAliases[k] ||= []).push(a); });
function pickDisplayAliasForKeyword(k){ return (keywordToAliases[k]&&keywordToAliases[k][0])||k; }
const uniqueKeywords=[...new Set(Object.values(aliasToKeyword))];
const baseItems=uniqueKeywords.map(k=>({display:pickDisplayAliasForKeyword(k), keyword:k}));
function filterItems(input){
const q=input.trim(); if(!q) return baseItems.slice();
const lower=q.toLowerCase(); const matched=new Set();
Object.keys(aliasToKeyword).forEach(a=>{ if(a.includes(q)||a.toLowerCase().includes(lower)) matched.add(aliasToKeyword[a]); });
uniqueKeywords.forEach(k=>{ if(k.includes(q)||k.toLowerCase().includes(lower)) matched.add(k); });
const list=[...matched].map(k=>({display:pickDisplayAliasForKeyword(k), keyword:k}));
list.sort((a,b)=>{
const ap=(a.display.startsWith(q)||a.keyword.startsWith(q))?0:1;
const bp=(b.display.startsWith(q)||b.keyword.startsWith(q))?0:1;
return ap-bp||a.display.localeCompare(b.display,'zh');
}); return list;
}
function makeIcon(kw,color){
const NS='http://www.w3.org/2000/svg';
const svg=document.createElementNS(NS,'svg'); svg.setAttribute('width','16'); svg.setAttribute('height','16'); svg.setAttribute('viewBox','0 0 24 24'); svg.setAttribute('fill','none'); svg.style.flex='0 0 auto';
const p=document.createElementNS(NS,'path'); p.setAttribute('stroke',color||'#9aa4b2'); p.setAttribute('stroke-width','2'); p.setAttribute('stroke-linecap','round'); p.setAttribute('stroke-linejoin','round');
const k=kw.toLowerCase();
if(['info','help','faq','summary','abstract','note'].some(s=>k.includes(s))) p.setAttribute('d','M12 8h.01M12 12v4m0 6a10 10 0 100-20 10 10 0 000 20z');
else if(['warning','caution','attention'].some(s=>k.includes(s))) p.setAttribute('d','M12 9v4m0 4h.01M10.29 3.86l-8.48 14.7A2 2 0 003.52 22h16.96a2 2 0 001.71-3.44L13.71 3.86a2 2 0 00-3.42 0z');
else if(['success','done','check'].some(s=>k.includes(s))) p.setAttribute('d','M9 12l2 2 4-4m7 2a9 9 0 11-18 0 9 9 0 0118 0z');
else if(['tip','hint','important'].some(s=>k.includes(s))) p.setAttribute('d','M12 2a7 7 0 00-7 7c0 2.76 1.67 5.14 4.06 6.21L9 19h6l-.06-3.79A7.002 7.002 0 0012 2zM9 22h6');
else if(['danger','failure','error','missing'].some(s=>k.includes(s))) p.setAttribute('d','M10 10l4 4m0-4l-4 4M12 22a10 10 0 100-20 10 10 0 000 20z');
else if(['question'].some(s=>k.includes(s))) p.setAttribute('d','M9 9a3 3 0 116 0c0 2-3 2-3 4m0 4h.01M12 22a10 10 0 100-20 10 10 0 000 20z');
else if(['example'].some(s=>k.includes(s))) p.setAttribute('d','M4 7h16M4 12h16M4 17h10');
else if(['quote','cite'].some(s=>k.includes(s))) p.setAttribute('d','M7 7h5v5H9a4 4 0 00-4 4v1M17 7h5v5h-3a4 4 0 00-4 4v1');
else if(['todo'].some(s=>k.includes(s))) p.setAttribute('d','M9 11l3 3L22 4M3 5h6M3 10h6M3 15h6M3 20h6');
else if(['bug'].some(s=>k.includes(s))) p.setAttribute('d','M14 7h-4a4 4 0 00-4 4v2a4 4 0 004 4h4a4 4 0 004-4v-2a4 4 0 00-4-4zM6 7l-2-2M18 7l2-2M6 17l-2 2M18 17l2 2');
else p.setAttribute('d','M12 8h.01M12 12v4m0 6a10 10 0 100-20 10 10 0 000 20z');
svg.appendChild(p); return svg;
}
let menuEl=null, menuVisible=false, activeIdx=-1, filteredItems=[];
let anchorTextarea=null, anchorTriggerIndex=-1, anchorLineIndex=-1;
function ensureBaseStyle(){
if(document.getElementById('callout-suggest-style')) return;
const st=document.createElement('style'); st.id='callout-suggest-style';
st.textContent=`
#callout-suggest-menu{
position:fixed; z-index:${Z_INDEX}; display:none; user-select:none;
border-radius:10px; border:1px solid rgba(255,255,255,0.16);
font-size:13px; line-height:1.6; padding:6px 0;
min-width:${MENU_WIDTH}px;
max-height:${MENU_MAX_H}px;
box-sizing:border-box;
overflow-y:auto; overflow-x:hidden;
backdrop-filter: blur(8px);
--fg:#e6edf3;
--bg0: rgba(30,41,59,0.88);
--bg1: rgba(17,24,39,0.88);
--scrollbar: rgba(148,163,184,0.45);
--scrollbar-hover: rgba(148,163,184,0.90);
--item-hover: rgba(255,255,255,0.06);
background: linear-gradient(180deg, var(--bg0) 0%, var(--bg1) 100%) !important;
color: var(--fg) !important;
color-scheme: dark;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar) transparent;
}
#callout-suggest-menu, #callout-suggest-menu * { color: var(--fg) !important; }
#callout-suggest-menu::-webkit-scrollbar{ width:8px; }
#callout-suggest-menu::-webkit-scrollbar-track{ background:transparent; }
#callout-suggest-menu::-webkit-scrollbar-thumb{ background-color:var(--scrollbar); border-radius:6px; }
#callout-suggest-menu:hover::-webkit-scrollbar-thumb{ background-color:var(--scrollbar-hover); }
#callout-suggest-menu .item{ padding:6px 10px; cursor:pointer; display:flex; align-items:center; gap:8px; }
#callout-suggest-menu .item:hover{ background: var(--item-hover) !important; }
#callout-suggest-menu .item > div{ flex:1 1 auto; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
`;
document.head.appendChild(st);
}
function ensureMenu(){
ensureBaseStyle();
if(menuEl) return menuEl;
const m=document.createElement('div'); m.id='callout-suggest-menu';
(document.getElementById('reply-control')||document.body).appendChild(m);
m.style.minWidth = MENU_WIDTH + 'px';
m.style.maxHeight = MENU_MAX_H + 'px';
m.style.overflowY = 'auto';
m.style.overflowX = 'hidden';
m.style.boxSizing = 'border-box';
menuEl=m; return m;
}
function applyActiveStyle(el,active){
const kw=el.dataset.keyword||''; const color=calloutColors[kw]||'#9aa4b2';
if(active){ el.style.background=hexToRGBA(color,0.18); el.style.borderLeft=`4px solid ${color}`; el.style.borderRadius='6px'; }
else { el.style.background=''; el.style.borderLeft='0'; }
}
function renderMenu(items){
ensureMenu(); menuEl.innerHTML='';
items.forEach((it,idx)=>{
const item=document.createElement('div'); item.className='item'; item.setAttribute('role','option');
item.dataset.index=String(idx); item.dataset.keyword=it.keyword;
const icon=makeIcon(it.keyword, calloutColors[it.keyword]||'#9aa4b2');
const text=document.createElement('div'); text.textContent=`${it.display} → ${it.keyword}`;
item.appendChild(icon); item.appendChild(text);
if(idx===activeIdx) applyActiveStyle(item,true);
item.addEventListener('mouseenter',()=> setActiveIdx(idx));
item.addEventListener('mousedown',e=>e.preventDefault());
item.addEventListener('click',()=> pickByKeyword(it.keyword));
menuEl.appendChild(item);
});
ensureActiveInView();
}
function ensureActiveInView(){
if(!menuVisible||!menuEl||activeIdx<0) return;
const node=menuEl.children[activeIdx]; if(!node||!node.scrollIntoView) return;
node.scrollIntoView({block:'nearest'});
}
function setActiveIdx(idx){
if(!filteredItems.length){ activeIdx=-1; return; }
if(idx<0) idx=0; if(idx>=filteredItems.length) idx=filteredItems.length-1;
activeIdx=idx; [...menuEl.children].forEach((el,i)=>applyActiveStyle(el,i===activeIdx)); ensureActiveInView();
}
function placeMenuAtViewportXY(x,y){
const margin=8; menuEl.style.display='block';
let left=Math.min(Math.max(margin,x),window.innerWidth-menuEl.offsetWidth-margin);
let top=y+4; const h=menuEl.offsetHeight;
if(top+h+margin>window.innerHeight) top=Math.max(margin,y-h-4);
menuEl.style.left=left+'px'; menuEl.style.top=top+'px';
}
function repositionMenu(){
if(!menuVisible||!anchorTextarea) return;
const {selectionStart}=anchorTextarea; const pos=getCaretPagePosition(anchorTextarea,selectionStart); placeMenuAtViewportXY(pos.x,pos.y);
}
/*** 打开/关闭 ***/
function openMenuAt(textarea,caretIndex,initialList){
ensureMenu(); filteredItems=initialList.slice(); activeIdx=filteredItems.length?0:-1;
renderMenu(filteredItems);
const pos=getCaretPagePosition(textarea,caretIndex); placeMenuAtViewportXY(pos.x,pos.y);
menuVisible=true; anchorTextarea=textarea;
textarea.focus(); // 防止任何意外失焦
}
function closeMenu(){
if(!menuEl) return; menuEl.style.display='none'; menuVisible=false;
filteredItems=[]; activeIdx=-1; anchorTextarea=null; anchorTriggerIndex=-1; anchorLineIndex=-1;
}
function buildCalloutLine(lines,idx,leadingSpaces,kw,title){
const prev=getPrevCalloutDepth(lines,idx); const depth=Math.max(1,prev+1);
return `${' '.repeat(leadingSpaces)}${'>'.repeat(depth)} [!${kw}]${title?' '+title:''}`;
}
function pickByKeyword(kw){
if(!anchorTextarea) return;
const ta = anchorTextarea;
const { text, lines, currentLineIndex, currentLine, lineStartIndex } = getEditorState(ta);
const relStart = Math.max(0, anchorTriggerIndex - lineStartIndex);
const caret = ta.selectionStart ?? (relStart + 1);
const prevDepth = getPrevCalloutDepth(lines, currentLineIndex);
const depth = Math.max(1, prevDepth + 1);
const leadingSpaces = (currentLine || '').length - (currentLine || '').trimStart().length;
const calloutLine = `${' '.repeat(leadingSpaces)}${'>'.repeat(depth)} [!${kw}]`;
const beforeTriggerInLine = currentLine.slice(0, relStart);
const afterCaretInLine = currentLine.slice(Math.max(0, caret - lineStartIndex));
const keptCurrentLine = beforeTriggerInLine + afterCaretInLine;
const textBeforeLine = text.slice(0, lineStartIndex);
const textAfterLine = text.slice(lineStartIndex + currentLine.length);
const newText = textBeforeLine + calloutLine + ' ' + keptCurrentLine + textAfterLine;
ta.value = newText;
const newCaret = textBeforeLine.length + calloutLine.length;
ta.selectionStart = ta.selectionEnd = newCaret;
ta.dispatchEvent(new Event('input', { bubbles: true }));
closeMenu();
ta.focus();
}
function onKeyDown(event){
const target=event.target; if(!(target instanceof HTMLTextAreaElement)) return;
if(event.key==='Tab'){
const st=getEditorState(target);
const triggerIdx = findTriggerBeforeCaret(target.value, st.lineStartIndex, st.selectionStart);
if(triggerIdx !== -1){
event.preventDefault(); event.stopPropagation(); if(event.stopImmediatePropagation) event.stopImmediatePropagation();
anchorTriggerIndex=triggerIdx; anchorLineIndex=st.currentLineIndex;
const upto=cleanInvisibles(target.value.slice(triggerIdx+1,st.selectionStart)).trimStart();
openMenuAt(target,st.selectionStart,filterItems(upto));
return;
}
const line=st.currentLine; const prefix=line.trim().split(/[\s::]/)[0]; const kw=aliasToKeyword[prefix];
if(kw){
event.preventDefault(); event.stopPropagation(); if(event.stopImmediatePropagation) event.stopImmediatePropagation();
const lead=line.length-line.trimStart().length;
const rest=line.trim().substring(prefix.length).trim().replace(/^[::]\s*/,'');
const newLine=buildCalloutLine(st.lines,st.currentLineIndex,lead,kw,rest);
updateEditor(target,st.lines,st.currentLineIndex,newLine,st.lineStartIndex);
target.focus();
return;
}
}
if(menuVisible){
if(event.key==='Escape'){ event.preventDefault(); closeMenu(); return; }
if(event.key==='ArrowDown'){ event.preventDefault(); setActiveIdx(activeIdx+1); return; }
if(event.key==='ArrowUp'){ event.preventDefault(); setActiveIdx(activeIdx-1); return; }
if(event.key==='Enter'){ if(activeIdx>=0){ event.preventDefault(); pickByKeyword(filteredItems[activeIdx].keyword); } return; }
if(event.key==='Tab'){ event.preventDefault(); return; }
}
}
function onInput(event){
if(!menuVisible||!anchorTextarea) return;
const target=event.target; if(target!==anchorTextarea) return;
const {selectionStart,currentLineIndex,lineStartIndex}=getEditorState(target);
if(currentLineIndex!==anchorLineIndex){ closeMenu(); return; }
if(anchorTriggerIndex<lineStartIndex || anchorTriggerIndex>=target.value.length){ closeMenu(); return; }
const between=target.value.slice(anchorTriggerIndex+1,selectionStart);
if(between.includes('\n')){ closeMenu(); return; }
// 再次确认触发符仍在
if(!TRIGGERS.includes(target.value[anchorTriggerIndex])){ closeMenu(); return; }
const q=cleanInvisibles(between).trim();
const next=filterItems(q); filteredItems=next.length?next:baseItems.slice();
activeIdx=filteredItems.length?0:-1; renderMenu(filteredItems);
const pos=getCaretPagePosition(target,selectionStart); placeMenuAtViewportXY(pos.x,pos.y);
target.focus(); // 保持焦点
}
function onDocMouseDown(e){
if(!menuVisible) return;
if(menuEl && !menuEl.contains(e.target)){
if(!(e.target instanceof HTMLTextAreaElement)) closeMenu();
}
}
document.addEventListener('keydown',onKeyDown,true);
document.addEventListener('input',onInput,true);
document.addEventListener('mousedown',onDocMouseDown,true);
})();
--【壹】--:
[!hint]好耶好耶!
--【贰】--:
[!success]真的很强
--【叁】--:
太强了,大佬
--【肆】--:
[!success]
效果很好!
--【伍】--:
[!note]
好用!
--【陆】--:
[!todo] 加一个IF.站的 测试了OK的
// @match https://idcflare.com/*
--【柒】--:
感谢佬分享!!
--【捌】--:
[!quote] 太强了佬
这下不得不支持了
--【玖】--:
爱站那边也可以用这个脚本哈???
--【拾】--:
[!abstract] 哦哟
哦哟
--【拾壹】--:
喜欢就好啊
--【拾贰】--:
给力,感谢分享
--【拾叁】--:
[!hint] 马上使用 很强大 感谢分享
--【拾肆】--:
[!info]
的确方便
--【拾伍】--:
[!success]-已用上,测试一下!
--【拾陆】--:
感谢佬友分享
--【拾柒】--:
[!tip]太牛了~这个更方便了
--【拾捌】--:
很不错,感谢佬友的分享。
--【拾玖】--:
[!hint]不错不错

