制作原因
- 习惯于 Obsidian 和 Github 的 Callout 样式
- 目前思源笔记的 Callout 使用方法不习惯(很麻烦)
特点
- 自动处理样式,不打断输入
- 纯 Javascript +CSS 实现,便于移植
使用方法
先把 Javascript 代码片段放到 设置-外观-代码片段-设置-JS
部分里。
再把 CSS 代码片段放到 设置-外观-代码片段-设置-CSS
部分里。
输入时和其他 callout 使用方法一样。
注:大小写无影响。
- note 样式,输入
>[!note]
- tip 样式,输入
>[!tip]
- important 样式,输入
>[!important]
- warning 样式,输入
>[!warning]
- caution 样式,输入
>[!caution]
演示 GIF
代码
以下是 JavaScript
// 在文件顶部添加日志,确认加载
console.log("脚本文件开始加载!(v15.1 - Commented out Debug Logs)"); // 版本号更新
// Debounce 工具函数 (带日志)
function debounce(func, wait) {
let timeout;
let funcName = func.name || 'anonymous';
return function(...args) {
const context = this;
const callTime = Date.now();
clearTimeout(timeout);
timeout = setTimeout(() => {
const execTime = Date.now();
const id = args[0] || 'Unknown'; // Assume first arg is mixedId
// console.log(`[Debounce] 执行 ${funcName} for ID: ${id} at ${execTime} (距离上次请求 ${execTime - callTime}ms).`); // DEBUG
func.apply(context, args);
}, wait);
};
}
// --- 状态跟踪 (统一,使用混合ID 和 上下文 Node ID) ---
const initializedContainers = new Set(); // 存储已初始化的容器 ID (docId or "preview_oid_Llevel")
const potentialBqBlocks = new Map(); // Map: mixedId -> Set<contextualNodeId> (e.g., "doc123||nodeABC")
const successfullyProcessedBlocks = new Map(); // Map: mixedId -> Set<contextualNodeId>
const processDebouncers = new Map(); // Map: mixedId -> debouncedFunction
const retryCounters = new Map(); // Map: mixedId -> retryCount
// --- 状态跟踪 Flags ---
let observer = null; // 保存 Observer 实例
// --- 常量 ---
const PROCESSING_DEBOUNCE_WAIT = 300; // ms - 处理队列的防抖延迟
const MAIN_DOC_INIT_SCAN_DELAY = 0; // ms - 主文档检测到 finished 后,延迟多久进行扫描 (可为 0)
const PREVIEW_INIT_SCAN_DELAY = 50; // ms - 预览窗口检测到 finished 后,延迟多久进行扫描 (推荐 > 0)
const RETRY_DELAY = 500; // ms - 评估失败后的重试延迟
const MAX_RETRIES = 5; // 最大重试次数
const PREVIEW_ID_PREFIX = "preview_"; // 用于预览窗口的混合 ID 前缀
const ID_SEPARATOR = "||"; // 用于分隔 mixedId 和 nodeId 的分隔符
const LOG_SEPARATOR = "____"; // 用于特定日志格式的分隔符
const LEVEL_SEPARATOR = "_L";
/** 清理元素样式 (通用) */
function clearCalloutStyles(element) {
const nodeId = element.dataset.nodeId || 'Unknown ID';
delete element.dataset.calloutType; delete element.dataset.calloutProcessed;
element.classList.remove('callout', 'callout-default', 'callout-note', 'callout-tip', 'callout-important', 'callout-warning', 'callout-caution');
element.querySelectorAll('[data-callout-title]').forEach(el => delete el.dataset.calloutTitle);
// console.log(`[ClearStyles ${nodeId}] Styles cleared.`); // DEBUG
}
/** 构造上下文 Node ID */
function createContextualNodeId(mixedId, nodeId) {
return `${mixedId}${ID_SEPARATOR}${nodeId}`;
}
/** 解析上下文 Node ID */
function parseContextualNodeId(contextualNodeId) {
const parts = contextualNodeId.split(ID_SEPARATOR);
if (parts.length === 2) { return { mixedId: parts[0], nodeId: parts[1] }; }
console.warn(`[Parse ID] Invalid contextualNodeId format: ${contextualNodeId}`); // 保留警告
return null;
}
/** 解析 Preview Mixed ID */
function parsePreviewMixedId(mixedId) {
if (!mixedId || !mixedId.startsWith(PREVIEW_ID_PREFIX)) {
return null;
}
const parts = mixedId.substring(PREVIEW_ID_PREFIX.length).split(LEVEL_SEPARATOR);
if (parts.length === 2) {
return { oid: parts[0], level: parts[1] };
}
console.warn(`[Parse Preview ID] Invalid preview mixedId format: ${mixedId}`); // 保留警告
return null;
}
/** 获取元素所属容器 Mixed ID (v15.0: Preview ID 包含 level) */
function getElementContainerMixedId(element) {
const popoverEl = element.closest('.block__popover[data-oid][data-level]');
if (popoverEl) {
const oid = popoverEl.dataset.oid;
const level = popoverEl.dataset.level;
if (oid && level) {
return `${PREVIEW_ID_PREFIX}${oid}${LEVEL_SEPARATOR}${level}`;
} else {
console.warn("[Get ID] Preview popover found but missing oid or level.", popoverEl); // 保留警告
return null;
}
} else {
const protyleEl = element.closest('.protyle[data-id]');
return protyleEl?.dataset.id || null;
}
}
/** 评估 Blockquote (通用) */
function evaluateBlockquote(bqElement, mixedId) {
const nodeId = bqElement?.dataset?.nodeId;
if (!bqElement || !(bqElement instanceof HTMLElement) || !nodeId || !mixedId || !initializedContainers.has(mixedId)) {
// console.warn(`[Evaluate ${mixedId}] Basic check failed.`); // DEBUG
return false;
}
const contextualNodeId = createContextualNodeId(mixedId, nodeId);
const successSet = successfullyProcessedBlocks.get(mixedId);
const potentialSet = potentialBqBlocks.get(mixedId);
if (bqElement.dataset.type !== 'NodeBlockquote') {
if (successSet?.has(contextualNodeId) || bqElement.dataset.calloutProcessed) {
// console.log(`[Evaluate ${mixedId}] Node ${nodeId}: Type changed. Clearing.`); // DEBUG
clearCalloutStyles(bqElement);
successSet?.delete(contextualNodeId);
}
potentialSet?.delete(contextualNodeId);
return false;
}
let textContainer = null; let foundViaPath = 'None';
const firstP = bqElement.querySelector(':scope > div[data-type="NodeParagraph"]:first-of-type');
if (firstP) { textContainer = firstP.firstElementChild; if (textContainer) foundViaPath = 'Path 1 (Standard)'; }
if (!textContainer) { const firstDivP = bqElement.querySelector(':scope > div.p:first-of-type'); if (firstDivP) { textContainer = firstDivP.firstElementChild; if(textContainer) foundViaPath = 'Path 2 (Compat .p)'; } }
if (!textContainer) { textContainer = bqElement.querySelector(':scope > div[contenteditable="true"]'); if (textContainer) { if(!textContainer.closest('.protyle-attr')) { foundViaPath = 'Path 3 (Direct ContentEditable)'; } else { textContainer = null; } } }
if (!textContainer || textContainer.nodeType !== Node.ELEMENT_NODE || textContainer.classList.contains('protyle-attr')) {
// console.log(`[Evaluate ${mixedId}] Node ${nodeId}: No suitable text container (Path: ${foundViaPath}).`); // DEBUG
if (bqElement.dataset.calloutProcessed === 'true') { clearCalloutStyles(bqElement); }
successSet?.delete(contextualNodeId);
return false;
}
// console.log(`[Evaluate ${mixedId}] Node ${nodeId}: Found text container via ${foundViaPath}.`); // DEBUG
const textContent = textContainer.textContent?.trim() ?? '';
const match = textContent.match(/^\[!([a-zA-Z]+)\]$/i);
const validTypes = ['note', 'tip', 'important', 'warning', 'caution'];
let determinedCalloutType = 'default';
if (match && match[1]) { const potentialType = match[1].toLowerCase(); if (validTypes.includes(potentialType)) { determinedCalloutType = potentialType; } }
const currentCalloutType = bqElement.dataset.calloutType;
const needsUpdate = bqElement.dataset.calloutProcessed !== 'true' || currentCalloutType !== determinedCalloutType;
if (needsUpdate) {
// console.log(`[Evaluate ${mixedId}] Node ${nodeId}: Applying/Updating type to '${determinedCalloutType}'.`); // DEBUG
clearCalloutStyles(bqElement);
bqElement.dataset.calloutType = determinedCalloutType;
bqElement.classList.add('callout', `callout-${determinedCalloutType}`);
// console.log(`[Style Applied] ${mixedId}${LOG_SEPARATOR}${nodeId} (Type: ${determinedCalloutType})`); // DEBUG (Style Applied Log)
bqElement.dataset.calloutProcessed = 'true';
const titleDiv = firstP?.querySelector(':scope > div[contenteditable="true"]');
if (titleDiv) titleDiv.dataset.calloutTitle = 'true';
} else {
// console.log(`[Evaluate ${mixedId}] Node ${nodeId}: No style update needed for type '${determinedCalloutType}'.`); // DEBUG
const titleDiv = firstP?.querySelector(':scope > div[contenteditable="true"][data-callout-title="true"]');
if(!titleDiv) {
const targetTitleDiv = firstP?.querySelector(':scope > div[contenteditable="true"]');
if (targetTitleDiv) targetTitleDiv.dataset.calloutTitle = 'true';
// console.log(`[Evaluate ${mixedId}] Node ${nodeId}: Re-applied missing data-callout-title.`); // DEBUG
}
}
return true;
}
/** Debounced Processing with Retry Logic (Unified) */
function processPotentialBqBlocksForContainer(mixedId) {
if (!initializedContainers.has(mixedId)) return;
const potentialBlocks = potentialBqBlocks.get(mixedId);
const successBlocks = successfullyProcessedBlocks.get(mixedId);
if (!potentialBlocks || !successBlocks) { console.error(`[ProcessQueue ${mixedId}] Error: Missing state maps.`); return; } // 保留错误
// console.log(`[ProcessQueue EXEC ${mixedId}] Queue size: ${potentialBlocks.size}. Content: [${Array.from(potentialBlocks).join(', ')}]`); // DEBUG
if (potentialBlocks.size === 0) { retryCounters.delete(mixedId); return; }
const processedIdsThisRun = new Set(); const failedIdsThisRun = new Set(); const blocksToProcess = new Set(potentialBlocks);
blocksToProcess.forEach(contextualNodeId => {
processedIdsThisRun.add(contextualNodeId);
const parsed = parseContextualNodeId(contextualNodeId);
if (!parsed || parsed.mixedId !== mixedId) {
// console.warn(`[ProcessQueue ${mixedId}] Mismatch/Invalid ID: ${contextualNodeId}. Removing.`); // DEBUG
potentialBlocks.delete(contextualNodeId); return;
}
const { nodeId: originalNodeId } = parsed;
const blockElement = document.querySelector(`[data-node-id="${originalNodeId}"]`);
let actualContainerId = null;
if (blockElement) actualContainerId = getElementContainerMixedId(blockElement);
if (blockElement && actualContainerId === mixedId) {
try {
const evaluationResult = evaluateBlockquote(blockElement, mixedId);
// console.log(`[ProcessQueue EVAL ${mixedId}] Node ${originalNodeId}: evaluateBlockquote returned ${evaluationResult}`); // DEBUG
if (evaluationResult) { successBlocks.add(contextualNodeId); potentialBlocks.delete(contextualNodeId); failedIdsThisRun.delete(contextualNodeId); }
else { failedIdsThisRun.add(contextualNodeId); successBlocks.delete(contextualNodeId); }
} catch (error) { console.error(`[ProcessQueue EXEC LOOP ${mixedId}] Node ${originalNodeId}: Error:`, error); successBlocks.delete(contextualNodeId); failedIdsThisRun.add(contextualNodeId); } // 保留错误
} else {
// console.log(`[ProcessQueue SKIP ${mixedId}] Node ${originalNodeId}: Element not found or container mismatch (Expected: ${mixedId}, Found: ${actualContainerId}). Removing.`); // DEBUG
successBlocks.delete(contextualNodeId); potentialBlocks.delete(contextualNodeId); failedIdsThisRun.delete(contextualNodeId);
}
});
if (potentialBlocks.size > 0) {
let currentRetries = retryCounters.get(mixedId) || 0;
if (currentRetries < MAX_RETRIES) {
currentRetries++; retryCounters.set(mixedId, currentRetries);
// console.log(`[ProcessQueue RETRY ${mixedId}] Scheduling retry ${currentRetries}/${MAX_RETRIES} in ${RETRY_DELAY}ms. Remaining: ${potentialBlocks.size}`); // DEBUG
setTimeout(() => { if (initializedContainers.has(mixedId)) { const processor = getDebouncedProcessor(mixedId); if (processor) processor(mixedId); } else { /*console.log(`[ProcessQueue RETRY ${mixedId}] Container closed before retry.`);*/ retryCounters.delete(mixedId); } }, RETRY_DELAY); // DEBUG in comment
} else { console.warn(`[ProcessQueue RETRY ${mixedId}] Max retries reached. ${potentialBlocks.size} blocks unprocessed.`); retryCounters.delete(mixedId); } // 保留警告
} else {
retryCounters.delete(mixedId);
// console.log(`[ProcessQueue EXEC ${mixedId}] Finished run. Potential queue empty.`); // DEBUG
}
// console.log(`[ProcessQueue EXEC ${mixedId}] Finished run. Success: ${successBlocks.size}, Remaining potential: ${potentialBlocks.size}`); // DEBUG
}
/** Function to get or create a debounced processor for a specific mixedId */
function getDebouncedProcessor(mixedId) {
if (!processDebouncers.has(mixedId)) {
const debouncedFunc = debounce(processPotentialBqBlocksForContainer, PROCESSING_DEBOUNCE_WAIT);
processDebouncers.set(mixedId, debouncedFunc);
// console.log(`[Debounce] Created debouncer for ${mixedId}`); // DEBUG
}
return processDebouncers.get(mixedId);
}
// --- Initialization and Cleanup Logic (Unified) ---
/** Runs the initial scan for blocks within a newly initialized container. */
function runScanForContainer(mixedId) {
if (!initializedContainers.has(mixedId)) {
// console.log(`[Initialize Scan ${mixedId}] Aborted, container no longer initialized.`); // DEBUG
return;
}
const potentialBlocks = potentialBqBlocks.get(mixedId);
const successBlocks = successfullyProcessedBlocks.get(mixedId);
if (!potentialBlocks || !successBlocks) { console.error(`[Initialize Scan ${mixedId}] Error: Missing state maps.`); return; } // 保留错误
let containerElement = null; let selector = '';
if (mixedId.startsWith(PREVIEW_ID_PREFIX)) {
const parsedPreviewId = parsePreviewMixedId(mixedId);
if (parsedPreviewId) {
const { oid, level } = parsedPreviewId;
selector = `.block__popover[data-oid="${oid}"][data-level="${level}"] .protyle[data-loading="finished"]`;
containerElement = document.querySelector(selector);
} else { console.error(`[Initialize Scan ${mixedId}] Failed to parse preview mixedId.`); return; } // 保留错误
} else {
selector = `.protyle[data-id="${mixedId}"][data-loading="finished"]`;
containerElement = document.querySelector(selector);
}
let initialBlocks = [];
if (containerElement) {
initialBlocks = containerElement.querySelectorAll(':scope div.bq[data-node-id]');
// console.log(`[Initialize Scan ${mixedId}] Found container: ${selector}. Found ${initialBlocks.length} BQ blocks.`); // DEBUG
} else {
console.warn(`[Initialize Scan ${mixedId}] Could not find container element using selector: ${selector}`); // 保留警告
return;
}
let addedCount = 0;
initialBlocks.forEach((bqElement) => {
const nodeId = bqElement.dataset.nodeId;
if (nodeId) { const contextualNodeId = createContextualNodeId(mixedId, nodeId); if (!successBlocks.has(contextualNodeId) && !potentialBlocks.has(contextualNodeId)) { potentialBlocks.add(contextualNodeId); addedCount++; } }
else { console.warn(`[Initialize Scan ${mixedId}] Block skipped, missing data-node-id.`); } // 保留警告
});
// console.log(`[Initialize Scan ${mixedId}] Scan finished. Added ${addedCount} new blocks. Queue size: ${potentialBlocks.size}`); // DEBUG
if (potentialBlocks.size > 0) {
// console.log(`[Initialize Scan ${mixedId}] Triggering process run for ${potentialBlocks.size} potential blocks.`); // DEBUG
const processor = getDebouncedProcessor(mixedId);
processor(mixedId);
}
}
/** Initializes a container (doc or preview) */
function initializeContainer(mixedId) {
if (initializedContainers.has(mixedId)) return;
// 保留这个主要的生命周期日志
console.log(`>>> [Initialize Container ${mixedId}] Starting initialization <<<`);
initializedContainers.add(mixedId);
potentialBqBlocks.set(mixedId, new Set());
successfullyProcessedBlocks.set(mixedId, new Set());
retryCounters.delete(mixedId);
const delay = mixedId.startsWith(PREVIEW_ID_PREFIX) ? PREVIEW_INIT_SCAN_DELAY : MAIN_DOC_INIT_SCAN_DELAY;
// console.log(`[Initialize Container ${mixedId}] Scheduling initial scan in ${delay}ms.`); // DEBUG
setTimeout(() => runScanForContainer(mixedId), delay);
}
/** Cleans up state for a specific container */
function cleanupContainer(mixedId) {
if (!initializedContainers.has(mixedId)) return;
// 保留这个主要的生命周期日志
console.log(`>>> [Cleanup Container ${mixedId}] Cleaning up state <<<`);
initializedContainers.delete(mixedId);
potentialBqBlocks.delete(mixedId);
successfullyProcessedBlocks.delete(mixedId);
retryCounters.delete(mixedId);
const debouncer = processDebouncers.get(mixedId);
if (debouncer && typeof debouncer.cancel === 'function') { debouncer.cancel(); }
processDebouncers.delete(mixedId);
// console.log(`[Cleanup Container ${mixedId}] State maps and debouncer removed.`); // DEBUG
}
// --- MutationObserver Setup ---
function setupObserver() {
if (observer) { observer.disconnect(); observer = null; }
// 保留 Observer 设置日志
console.log(">>> [Setup Observer v15.1] Setting up MutationObserver <<<"); // 版本更新
const targetNode = document.body;
if (!targetNode) { console.error("[Setup Observer] Observer target (document.body) not found!"); return; } // 保留错误
const config = { childList: true, subtree: true, attributes: true, attributeFilter: ['data-loading', 'data-oid', 'data-level', 'class', 'data-type', 'data-node-id'], characterData: true };
const callback = function(mutationsList, obs) {
const timeStamp = Date.now();
let containersToProcess = new Set();
for (const mutation of mutationsList) {
// --- 1. Detect Container Load/Unload ---
if (mutation.type === 'attributes' && mutation.attributeName === 'data-loading') {
const targetElement = mutation.target;
if (targetElement?.matches?.('.protyle')) {
const protyleEl = targetElement;
const containerId = getElementContainerMixedId(protyleEl);
const currentLoadingState = protyleEl.dataset.loading;
// v15.1: 注释掉详细的属性变化调试日志
// let sourceDesc = 'Unknown Container';
// if (containerId) { ... } else { ... }
// console.log(`[Observer Debug - Attr] data-loading changed on .protyle within ${sourceDesc}. New state: ${currentLoadingState}. Calculated containerId: ${containerId}`); // DEBUG
if (containerId) {
const isInitialized = initializedContainers.has(containerId);
// console.log(`[Observer Debug - Attr] Container ${containerId} initialized status: ${isInitialized}`); // DEBUG
if (currentLoadingState === 'finished' && !isInitialized) {
// console.log(`[Observer Debug - Attr] Triggering initializeContainer for ${containerId}`); // DEBUG
initializeContainer(containerId);
} else if (currentLoadingState !== 'finished' && isInitialized) {
// console.log(`[Observer Debug - Attr] Triggering cleanupContainer for ${containerId}`); // DEBUG
cleanupContainer(containerId);
} else if (currentLoadingState === 'finished' && isInitialized) {
// console.log(`[Observer Debug - Attr] Container ${containerId} already initialized. Ignoring 'finished' signal.`); // DEBUG
}
}
}
}
// --- 2. Detect Preview Add/Remove ---
else if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.matches?.('.block__popover[data-oid][data-level]')) {
const oid = node.dataset.oid;
const level = node.dataset.level;
// console.log(`[Observer Debug - ChildList] Detected Preview Popover ADDED (OID: ${oid}, Level: ${level})`); // DEBUG
}
});
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches?.('.block__popover[data-oid][data-level]')) {
const oid = node.dataset.oid;
const level = node.dataset.level;
const mixedId = `${PREVIEW_ID_PREFIX}${oid}${LEVEL_SEPARATOR}${level}`;
// console.log(`[Observer Debug - ChildList] Detected Preview Popover REMOVED (OID: ${oid}, Level: ${level}). Cleaning up container: ${mixedId}`); // DEBUG
cleanupContainer(mixedId);
}
else if (node.matches?.('.protyle[data-id]')) {
const docId = node.dataset.id;
if (initializedContainers.has(docId)) {
// console.log(`[Observer Debug - ChildList] Detected Doc Protyle REMOVED (${docId}). Cleaning up.`); // DEBUG
cleanupContainer(docId);
}
}
}
});
}
// --- 3. Process Block Changes ---
let affectedBqElement = null; let targetElement = mutation.target;
if (mutation.type === 'childList') { const nodes = [...mutation.addedNodes, ...mutation.removedNodes]; for (const node of nodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches && node.matches('div.bq[data-node-id]')) { affectedBqElement = node; break; } const parentBq = node.parentElement?.closest('div.bq[data-node-id]'); if (parentBq) { affectedBqElement = parentBq; break; } } else if (node.nodeType === Node.TEXT_NODE) { affectedBqElement = node.parentElement?.closest('div.bq[data-node-id]'); if (affectedBqElement) break; } } if (!affectedBqElement && targetElement.nodeType === Node.ELEMENT_NODE && targetElement.matches('div.bq[data-node-id]')) { affectedBqElement = targetElement; } }
else if (mutation.type === 'attributes') { if (targetElement.nodeType === Node.ELEMENT_NODE) { affectedBqElement = targetElement.closest('div.bq[data-node-id]'); } }
else if (mutation.type === 'characterData') { if (targetElement.parentElement) { affectedBqElement = targetElement.parentElement.closest('div.bq[data-node-id]'); } }
if (affectedBqElement && affectedBqElement.dataset.nodeId) {
const nodeId = affectedBqElement.dataset.nodeId;
const containerId = getElementContainerMixedId(affectedBqElement);
if (containerId && initializedContainers.has(containerId)) {
const potentialBlocks = potentialBqBlocks.get(containerId);
const successBlocks = successfullyProcessedBlocks.get(containerId);
const contextualNodeId = createContextualNodeId(containerId, nodeId);
if (potentialBlocks && successBlocks) {
// console.log(`[Observer ${timeStamp}] Node ${nodeId} (Container ${containerId}): Change detected. Adding to potential queue.`); // DEBUG
if (successBlocks.has(contextualNodeId)) { successBlocks.delete(contextualNodeId); }
if (!potentialBlocks.has(contextualNodeId)) { potentialBlocks.add(contextualNodeId); }
containersToProcess.add(containerId);
if (mutation.type === 'attributes' && mutation.attributeName === 'data-type' && affectedBqElement.dataset.type !== 'NodeBlockquote') {
// console.log(`[Observer ${timeStamp}] Node ${nodeId} (Container ${containerId}): Type changed away. Cleaning up.`); // DEBUG
clearCalloutStyles(affectedBqElement); successBlocks.delete(contextualNodeId); potentialBlocks.delete(contextualNodeId); containersToProcess.delete(containerId);
}
}
} /* else { // DEBUG
if(containerId) console.log(`[Observer ${timeStamp}] Node ${nodeId} (Container ${containerId}): Change detected, but container not initialized.`);
else console.log(`[Observer ${timeStamp}] Node ${nodeId}: Change detected, but could not determine valid container.`);
} */
}
} // End mutation loop
containersToProcess.forEach(mixedId => {
if (initializedContainers.has(mixedId)) {
const processor = getDebouncedProcessor(mixedId);
if (processor) {
// console.log(`[Observer Trigger] Triggering debounced processor for ${mixedId}`); // DEBUG
processor(mixedId);
}
}
});
};
observer = new MutationObserver(callback);
try {
observer.observe(targetNode, config);
// 保留启动成功日志
console.log("[Setup Observer v15.1] MutationObserver started successfully.");
} catch (error) {
console.error("[Setup Observer v15.1] Failed to start MutationObserver:", error); // 保留错误
observer = null;
}
}
function startup() {
// 保留启动日志
console.log(">>> [Startup v15.1] Initializing or restarting script <<<"); // 版本更新
initializedContainers.clear(); potentialBqBlocks.clear(); successfullyProcessedBlocks.clear();
processDebouncers.clear(); retryCounters.clear();
if (observer) {
// console.log("[Startup v15.1] Disconnecting existing observer."); // DEBUG
observer.disconnect();
observer = null;
}
setupObserver();
}
// --- 启动逻辑 ---
// 保留 DOM 状态检查日志
console.log("正在检查 document.readyState:", document.readyState);
if (document.readyState === 'loading') {
console.log("DOM not ready, adding DOMContentLoaded listener.");
document.addEventListener('DOMContentLoaded', startup);
} else {
console.log("DOM ready, executing startup directly.");
startup();
}
// 保留脚本结束日志
console.log("Script execution finished. (v15.1)"); // 版本更新
以下是 CSS
/* --- Base Callout Styles (Applied to any block with data-callout-type) --- */
.bq[data-callout-type] {
/* Common styles like padding, border-radius, text color */
padding: 15px !important;
border-radius: 0;
color: var(--b3-theme-on-background);
/* Add any other common styles here */
}
/* Style the first paragraph (title container) */
.bq[data-callout-type] > div[data-type="NodeParagraph"]:first-of-type {
margin-bottom: 0.5em; /* Adjust spacing between title and content if needed */
}
/* Style the contenteditable div within the first paragraph (where [!TYPE] was) */
.bq[data-callout-type] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"] {
display: inline-flex; /* Used for aligning icon and text */
align-items: center;
font-weight: bold; /* Optional: Make the title bold */
margin-bottom: 0; /* Prevent default paragraph margins on this specific div */
}
/* --- Specific Callout Types (Using data-callout-type) --- */
/* Note */
.bq[data-callout-type="note"] {
border-left: .25em solid #1f6feb;
background-color: #1f71eb16 !important;
}
/* Select the specific inner div for the icon and title color */
.bq[data-callout-type="note"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"] {
color: #4493f8; /* Title text color */
}
/* Style the icon using ::before */
.bq[data-callout-type="note"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"]::before {
content: ""; /* Required for ::before */
display: inline-block;
overflow: visible !important;
vertical-align: middle;
width: 22px;
height: 22px;
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjcxNDI4NTcxNDI4NTcxNDIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imx1Y2lkZSBsdWNpZGUtY2lyY2xlLWFsZXJ0Ij48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxsaW5lIHgxPSIxMiIgeDI9IjEyIiB5MT0iOCIgeTI9IjEyIi8+PGxpbmUgeDE9IjEyIiB4Mj0iMTIuMDEiIHkxPSIxNiIgeTI9IjE2Ii8+PC9zdmc+");
mask-size: cover;
background-color: currentColor; /* Inherits color from the parent (title text color) */
margin-right: 10px;
mask-repeat: no-repeat;
}
/* Tip */
.bq[data-callout-type="tip"] {
border-left: .25em solid #238636;
background-color: #23863615 !important;
}
.bq[data-callout-type="tip"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"] {
color: #238636;
}
.bq[data-callout-type="tip"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"]::before {
content: "";
display: inline-block;
overflow: visible !important;
vertical-align: middle;
width: 22px;
height: 22px;
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjcxNDI4NTcxNDI4NTcxNDIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imx1Y2lkZSBsdWNpZGUtbGlnaHRidWxiIj48cGF0aCBkPSJNMTUgMTRjLjItMSAuNy0xLjcgMS41LTIuNSAxLS45IDEuNS0yLjIgMS41LTMuNUE2IDYgMCAwIDAgNiA4YzAgMSAuMiAyLjIgMS41IDMuNS43LjcgMS4zIDEuNSAxLjUgMi41Ii8+PHBhdGggZD0iTTkgMThoNiIvPjxwYXRoIGQ9Ik0xMCAyMmg0Ii8+PC9zdmc+");
mask-size: cover;
background-color: currentColor;
margin-right: 10px;
mask-repeat: no-repeat;
}
/* Important */
.bq[data-callout-type="important"] {
border-left: .25em solid #8957e5;
background-color: #8957e515 !important;
}
.bq[data-callout-type="important"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"] {
color: #8957e5;
}
.bq[data-callout-type="important"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"]::before {
content: "";
display: inline-block;
overflow: visible !important;
vertical-align: middle;
width: 22px;
height: 22px;
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjcxNDI4NTcxNDI4NTcxNDIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imx1Y2lkZSBsdWNpZGUtbWVzc2FnZS1zcXVhcmUtd2FybmluZyI+PHBhdGggZD0iTTIxIDE1YTIgMiAwIDAgMS0yIDJIN2wtNCA0VjVhMiAyIDAgMCAxIDItMmgxNGEyIDIgMCAwIDEgMiAyeiIvPjxwYXRoIGQ9Ik0xMiA3djIiLz48cGF0aCBkPSJNMTIgMTNoLjAxIi8+PC9zdmc+");
mask-size: cover;
background-color: currentColor;
margin-right: 10px;
mask-repeat: no-repeat;
}
/* Warning */
.bq[data-callout-type="warning"] {
border-left: .25em solid #9e6a03;
background-color: #9e6a0315 !important;
}
.bq[data-callout-type="warning"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"] {
color: #9e6a03;
}
.bq[data-callout-type="warning"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"]::before {
content: "";
display: inline-block;
overflow: visible !important;
vertical-align: middle;
width: 22px;
height: 22px;
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjcxNDI4NTcxNDI4NTcxNDIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imx1Y2lkZSBsdWNpZGUtdHJpYW5nbGUtYWxlcnQiPjxwYXRoIGQ9Im0yMS43MyAxOC04LTE0YTIgMiAwIDAgMC0zLjQ4IDBsLTggMTRBMiAyIDAgMCAwIDQgMjFoMTZhMiAyIDAgMCAwIDEuNzMtMyIvPjxwYXRoIGQ9Ik0xMiA5djQiLz48cGF0aCBkPSJNMTIgMTdoLjAxIi8+PC9zdmc+");
mask-size: cover;
background-color: currentColor;
margin-right: 10px;
mask-repeat: no-repeat;
}
/* Caution / Danger */
.bq[data-callout-type="caution"] {
border-left: .25em solid #da3633;
background-color: #da363315 !important;
}
.bq[data-callout-type="caution"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"] {
color: #da3633;
}
.bq[data-callout-type="caution"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"]::before {
content: "";
display: inline-block;
overflow: visible !important;
vertical-align: middle;
width: 22px;
height: 22px;
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjcxNDI4NTcxNDI4NTcxNDIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imx1Y2lkZSBsdWNpZGUtb2N0YWdvbi1hbGVydCI+PHBhdGggZD0iTTEyIDE2aC4wMSIvPjxwYXRoIGQ9Ik0xMiA4djQiLz48cGF0aCBkPSJNMTUuMzEyIDJhMiAyIDAgMCAxIDEuNDE0LjU4Nmw0LjY4OCA0LjY4OEEyIDIgMCAwIDEgMjIgOC42ODh2Ni42MjRhMiAyIDAgMCAxLS41ODYgMS40MTRsLTQuNjg4IDQuNjg4YTIgMiAwIDAgMS0xLjQxNC41ODZIOC42ODhhMiAyIDAgMCAxLTEuNDE0LS41ODZsLTQuNjg4LTQuNjg4QTIgMiAwIDAgMSAyIDE1LjMxMlY4LjY4OGEyIDIgMCAwIDEgLjU4Ni0xLjQxNGw0LjY4OC00LjY4OEEyIDIgMCAwIDEgOC42ODggMnoiLz48L3N2Zz4=");
mask-size: cover;
background-color: currentColor;
margin-right: 10px;
mask-repeat: no-repeat;
}
/* Default Callout (when no valid [!TYPE] is found) */
.bq[data-callout-type="default"] {
border-left: .25em solid #7d8590; /* Example: GitHub's default blockquote color */
background-color: #7d859015 !important; /* Example background */
/* You might choose NOT to have an icon for default, or add a generic one */
}
/* Optional: Style the title div for default if needed */
.bq[data-callout-type="default"] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"] {
color: #7d8590; /* Example title color */
/* If adding an icon for default: */
/* &::before { ... icon styles ... } */
}
/* Optional: Hide the original [!TYPE] text visually */
/* If you want to hide the "[!TYPE]" text itself but keep the icon and maybe a generated title */
/* This targets the contenteditable div that contains "[!TYPE]" */
/* Make sure this is commented out or removed if you WANT the "[!TYPE]" text to be visible */
/*
.bq[data-callout-type] > div[data-type="NodeParagraph"]:first-of-type > div[contenteditable="true"] {
display: none;
/* Alternative visual hiding */
/*
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
*/
代码说明
-
通过识别
data-type
等字段来实现 callout 样式的自动设置 -
100% 代码来自于 Gemini-2.5-pro-preview-03-25。
-
重要说明
- 本人不负责修任何 BUG,发完帖子 99% 概率不会再管。
- 有任何需求直接问 AI。
已知 BUG
BUG
- 可能在重启后,或者新建文档后,第一次输入 Callout 会出现无法自动应用样式的问题。
解决办法
- 按 f5 刷新
可能原因
- 根据 AI 分析(Gemini+Claude+Deepseek),均认为是思源笔记渲染笔记时微妙的渲染时间差异导致。
- 无法从根本上解决。
测试案例
测试过如下主题。
- 思源笔记官方自带主题
- Asri 主题
其他主题没测试过,理论上不应该有问题。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于