可视化的油猴的 Obsidian Callouts

2026-04-11 14:471阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

基于佬友的脚本做了点微小的改进,同时支持输入中文别名,按下Tab键完成转换

搞一点油猴的 Obsidian Callouts 开发调优
油猴脚本 安装脚本并刷新页面后,输入中文别名,按下Tab键完成转换并可预览效果 [20250919-0147-59.0972439] 中文别名 callouts 效果(描述) 笔记 note 蓝色信息笔记,通常带有灯泡或笔记图标,用于一般备注。 摘要 abstract 青色摘要框,用于总结或概要内容。 概要 abstract 青色摘要框,用于总结或概要内容。 总…

先看演示

最近更新:

支持可视化的油猴的 Obsidian Callouts - #62,来自 Jenhy 功能实现

支持idc flare里使用

主要改进点

  1. 中英符号唤起,支持输入>或者》后,再按 Tab 唤起

  2. 可视化展示,icon、颜色预览,无需输入完成后才能看到效果
    image273×274 11.5 KB

  3. 快速检索,中英支持
    image292×191 7.63 KB
    image287×126 4.16 KB

  4. 自动层级嵌套(不建议嵌套的花里胡哨,影响佬友看帖)
    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里使用

主要改进点

  1. 中英符号唤起,支持输入>或者》后,再按 Tab 唤起

  2. 可视化展示,icon、颜色预览,无需输入完成后才能看到效果
    image273×274 11.5 KB

  3. 快速检索,中英支持
    image292×191 7.63 KB
    image287×126 4.16 KB

  4. 自动层级嵌套(不建议嵌套的花里胡哨,影响佬友看帖)
    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]不错不错

标签:配置优化