---
name: Callout 核心引擎
description: 后台引擎:拦截并渲染携带 Callout 标记的元素,提供高级容器视觉和内部文本自适应排版。
author: ymjr
version: 1.0.0
license: MIT
usage: 后台常驻引擎。为所有包含 `customData.callout` 的元素提供底层渲染能力。
features:
- 拦截 `drawElementOnCanvas` 自定义绘制带侧边栏和图标的 Callout UI
- 拦截 `beforeStartTextEditing` 等钩子实现 Callout 文本向右偏移排版和自适应边界
- 维持 Excalidraw 原生选中框和文本编辑器一致性
dependencies:
- 独立运行,供 Callout 系列的 Action 动作脚本调用
autorun: true
---
/*
```javascript
*/
var locales = {
zh: {
log_unmounted: "[{id}] 🔌 已卸载",
log_mounted: "[{id}] 🚀 挂载完毕"
},
en: {
log_unmounted: "[{id}] 🔌 Unmounted",
log_mounted: "[{id}] 🚀 Mounted successfully"
}
};
const SCRIPT_ID = "ymjr.feature.callout-engine";
// ====================== 静态资源与常量 ======================
const CALLOUT_ICONS_SVG = {
warning: ``,
note: ``,
info: ``,
tip: ``,
success: ``,
failure: ``,
cite: ``,
quote: ``,
example: ``,
danger: ``,
bug: ``,
question: ``,
abstract: ``
};
const CALLOUT_BG_COLORS = {
note: "#DAE9FA", info: "#DAE9FA", tip: "#D9F5F5", success: "#DAF5E5", failure: "#FCE0E4", warning: "#FCEAD9",
error: "#FADABD", quote: "#F1F1F1", cite: "#F1F1F1", example: "#EBE5FC", danger: "#FCE0E4", bug: "#FCE0E4",
question: "#FCEAD9", abstract: "#D9F5F5"
};
const CALLOUT_ICON_COLORS = {
note: "#1775D9", info: "#1775D9", tip: "#16A6AB", success: "#1DA51D", failure: "#DD2C38", warning: "#DE7417",
error: "#DD2C38", quote: "#9E9E9E", cite: "#9E9E9E", example: "#8F47E1", danger: "#DD2C38", bug: "#DD2C38",
question: "#DE7417", abstract: "#16A6AB"
};
const CALLOUT_HEADER_HEIGHT = 36;
const CALLOUT_STRIP_WIDTH = 6;
const hexToRgba = (hex, alpha) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
window._calloutIconCache = window._calloutIconCache || new Map();
const getColoredIconImage = (type, color, ea, api) => {
const cacheKey = `${type}-${color}`;
if (window._calloutIconCache.has(cacheKey)) {
return window._calloutIconCache.get(cacheKey);
}
const svgRaw = CALLOUT_ICONS_SVG[type] || CALLOUT_ICONS_SVG.note;
let svgColored = svgRaw;
if (svgColored.includes('currentColor')) {
svgColored = svgColored.replaceAll('currentColor', color);
} else if (svgColored.includes('fill="')) {
svgColored = svgColored.replace(/fill="[^"]*"/g, `fill="${color}"`);
} else {
svgColored = svgColored.replace('