paper-burner/js/annotations/annotation_highlighter.js

2044 lines
96 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 应用块级或子块级元素的高亮和交互。
* 容器中的任何块级元素(段落、标题、列表等)应具有 'data-block-index' 属性。
* 块级元素内部的子块 (span) 应具有 'data-sub-block-id' 属性 (格式如 'parentIndex.subIndex')。
* 批注应通过 'ann.target.selector[0].subBlockId' 或 'ann.target.selector[0].blockIndex' 来定位这些元素。
*
* @param {HTMLElement} containerElement - 内容的父容器元素。
* @param {Array<Object>} allAnnotations - 文档的所有批注数据列表。
* @param {string} contentIdentifier - 当前内容类型的标识符 ('ocr' 或 'translation')。
*/
function applyBlockAnnotations(containerElement, allAnnotations, contentIdentifier) {
const __ANNOTATION_DEBUG__ = (function(){
try { return !!(window && (window.ENABLE_ANNOTATION_DEBUG || localStorage.getItem('ENABLE_ANNOTATION_DEBUG') === 'true')); } catch { return false; }
})();
// ====== 调用时机日志 ======
// console.log('[applyBlockAnnotations] 被调用', {
// contentIdentifier,
// containerElement,
// annotationCount: allAnnotations ? allAnnotations.length : 0,
// callStack: (new Error().stack)
//});
// ====== 新增4.1日志(入口) ======
try {
const sub41 = containerElement.querySelector('.sub-block[data-sub-block-id="4.1"]');
if (sub41) {
//console.log('[调试][入口] 4.1 outerHTML:', sub41.outerHTML);
//console.log('[调试][入口] 4.1 textContent:', sub41.textContent);
} else {
//console.log('[调试][入口] 4.1 不存在');
}
} catch (e) { console.error('[调试][入口] 4.1 日志异常', e); }
// ====== 新增:高亮应用前全局检测 ======
if (__ANNOTATION_DEBUG__) {
const allBlocksPre = containerElement.querySelectorAll('[data-block-index]');
allBlocksPre.forEach(block => {
const subs = block.querySelectorAll('.sub-block');
// console.log(`[检测][applyBlockAnnotations前] block#${block.dataset.blockIndex} 有${subs.length}个sub-block:`, Array.from(subs).map(sb => (sb.textContent || '').substring(0, 20)));
});
}
// 打印所有 sub-block 的内容(分割后)
if (__ANNOTATION_DEBUG__) {
if (containerElement) {
const allSubBlocks = containerElement.querySelectorAll('.sub-block');
allSubBlocks.forEach(sb => {
// console.log('[applyBlockAnnotations][分割后] sub-block', sb.dataset.subBlockId, '内容:', sb.textContent);
});
}
}
// ====== 日志:高亮应用开始 ======
//console.log(`[BlockHighlighter] 开始应用高亮contentIdentifier=${contentIdentifier}`);
const allSubBlocks = containerElement.querySelectorAll('.sub-block');
//console.log(`[BlockHighlighter] 当前子块数量: ${allSubBlocks.length}`);
//console.log(`[BlockHighlighter] 当前子块ID:`, Array.from(allSubBlocks).map(sb => sb.dataset.subBlockId));
performance.mark('applyAnnotations-start');
if (window.currentVisibleTabId === 'chunk-compare') {
return;
}
if (!containerElement || !allAnnotations) {
//console.log("[BlockHighlighter] 未提供容器元素或批注数据。");
return;
}
// ====== 日志:去重前 ======
//console.log('[BlockHighlighter] 去重前 annotation 数量:', allAnnotations.length);
const beforeAnnList = __ANNOTATION_DEBUG__ ? allAnnotations.map(ann => ann.target && ann.target.selector && ann.target.selector[0] && ann.target.selector[0].subBlockId).filter(Boolean) : [];
//console.log('[BlockHighlighter] 去重前 subBlockId 列表:', beforeAnnList);
// ========== 去重 ==========
const seen = new Set();
const dedupedAnnotations = [];
for (const ann of allAnnotations) {
if (ann.targetType !== contentIdentifier) {
dedupedAnnotations.push(ann);
continue;
}
let key = '';
if (ann.target && ann.target.selector && ann.target.selector[0]) {
if (ann.target.selector[0].subBlockId) {
if (ann.target.selector[0].type === 'SubBlockRangeSelector' && (Number.isFinite(ann.target.selector[0].startOffset) || Number.isFinite(ann.target.selector[0].endOffset))) {
key = 'subBlockRange:' + ann.target.selector[0].subBlockId + ':' + (ann.target.selector[0].startOffset || 0) + ':' + (ann.target.selector[0].endOffset || 0);
} else {
key = 'subBlockId:' + ann.target.selector[0].subBlockId;
}
} else if (ann.target.selector[0].blockIndex !== undefined) {
key = 'blockIndex:' + ann.target.selector[0].blockIndex;
}
}
if (!key) {
dedupedAnnotations.push(ann);
continue;
}
if (!seen.has(key)) {
seen.add(key);
dedupedAnnotations.push(ann);
}
}
allAnnotations = dedupedAnnotations;
// ====== 日志:去重后 ======
//console.log('[BlockHighlighter] 去重后 annotation 数量:', allAnnotations.length);
const afterAnnList = __ANNOTATION_DEBUG__ ? allAnnotations.map(ann => ann.target && ann.target.selector && ann.target.selector[0] && ann.target.selector[0].subBlockId).filter(Boolean) : [];
//console.log('[BlockHighlighter] 去重后 subBlockId 列表:', afterAnnList);
// ====== 日志:清理高亮前 ======
const beforeCleanSubBlocks = __ANNOTATION_DEBUG__ ? containerElement.querySelectorAll('.sub-block') : [];
//console.log(`[BlockHighlighter] 清理前子块数量: ${beforeCleanSubBlocks.length}`);
// 1. 只移除高亮样式和属性,不删除子块
const existingAnnotationSpans = containerElement.querySelectorAll('[data-annotation-id]');
function hasSubBlockDescendant(node) {
if (!node || !node.querySelectorAll) return false;
return node.querySelectorAll('.sub-block').length > 0;
}
existingAnnotationSpans.forEach(span => {
if (span.classList.contains('sub-block')) {
// 只移除高亮样式和批注属性,不删除子块
span.style.backgroundColor = '';
span.style.border = '';
span.style.padding = '';
span.style.borderRadius = '';
span.style.boxShadow = '';
span.classList.remove('annotated-sub-block', 'has-note', 'cross-block-highlight');
span.removeAttribute('data-annotation-id');
span.removeAttribute('title');
// 移除跨子块标识
const indicator = span.querySelector('.cross-block-indicator');
if (indicator) indicator.remove();
} else if (span.classList.contains('annotation-wrapper')) {
const parent = span.parentNode;
// 清掉内部的跨子块指示器
const indicator = span.querySelector('.cross-block-indicator');
if (indicator) indicator.remove();
while (span.firstChild) {
parent.insertBefore(span.firstChild, span);
}
span.remove();
} else if (span.classList.contains('pre-annotated')) {
// 只清理属性,不删除节点
span.style.backgroundColor = '';
span.style.border = '';
span.style.padding = '';
span.style.borderRadius = '';
span.style.boxShadow = '';
span.classList.remove('pre-annotated', 'annotated-block', 'annotated-sub-block', 'has-annotation', 'has-highlight', 'has-note', 'cross-block-highlight');
span.removeAttribute('data-annotation-id');
span.removeAttribute('data-highlight-color');
span.removeAttribute('title');
} else {
// 递归检测所有后代是否包含 .sub-block
if (hasSubBlockDescendant(span)) {
if (__ANNOTATION_DEBUG__) console.warn('[高亮清理保护-递归] 跳过包含 sub-block 的节点:', span.outerHTML);
return;
} else {
// 通用“安全解包”:保留文本内容,不删正文
const parent = span.parentNode;
// 移除内部跨子块指示器
const indicator = span.querySelector && span.querySelector('.cross-block-indicator');
if (indicator) indicator.remove();
if (span.childNodes && span.childNodes.length > 0) {
while (span.firstChild) {
parent.insertBefore(span.firstChild, span);
}
span.remove();
} else {
// 仅文本内容(或为空):将文本节点放回去
const text = span.textContent || '';
if (text) parent.insertBefore(document.createTextNode(text), span);
span.remove();
}
}
}
});
// 2. 移除所有带有批注相关类的元素上的类和样式
const existingHighlights = containerElement.querySelectorAll('.annotated-block, .annotated-sub-block');
existingHighlights.forEach(el => {
el.style.backgroundColor = '';
el.style.border = '';
el.style.padding = '';
el.style.borderRadius = '';
el.style.boxShadow = '';
if (el.classList.contains('katex-display') || el.tagName === 'IMG' || el.tagName === 'TABLE') {
el.style.display = '';
el.style.width = '';
el.style.marginLeft = '';
el.style.marginRight = '';
}
const katexChild = el.querySelector('.katex-display');
if (katexChild) {
katexChild.style.border = ''; katexChild.style.padding = '';
katexChild.style.display = ''; katexChild.style.width = '';
katexChild.style.marginLeft = ''; katexChild.style.marginRight = '';
katexChild.style.borderRadius = ''; katexChild.style.boxShadow = '';
}
const imgChild = el.querySelector('img');
if (imgChild) {
imgChild.style.border = ''; imgChild.style.padding = '';
imgChild.style.display = ''; imgChild.style.width = '';
imgChild.style.marginLeft = ''; imgChild.style.marginRight = '';
imgChild.style.borderRadius = ''; imgChild.style.boxShadow = '';
}
const tableChild = el.querySelector('table');
if (tableChild) {
tableChild.style.border = ''; tableChild.style.padding = '';
tableChild.style.display = ''; tableChild.style.width = '';
tableChild.style.marginLeft = ''; tableChild.style.marginRight = '';
tableChild.style.borderRadius = ''; tableChild.style.boxShadow = '';
}
el.classList.remove('annotated-block', 'annotated-sub-block', 'has-annotation', 'has-highlight', 'cross-block-highlight');
if (el.hasAttribute('data-annotation-id')) {
el.removeAttribute('data-annotation-id');
}
if (el.hasAttribute('data-highlight-color')) {
el.removeAttribute('data-highlight-color');
}
if (el.tagName === 'SPAN' && el.classList.contains('sub-block') &&
el.parentElement && el.parentElement.tagName === 'TABLE') {
const subBlockId = el.dataset.subBlockId;
if (subBlockId) {
const subBlockIdPrefix = subBlockId.split('.')[0];
if (el.parentElement.dataset.blockIndex === subBlockIdPrefix) {
el.parentElement.style.border = '';
el.parentElement.style.padding = '';
el.parentElement.style.display = '';
el.parentElement.style.width = '';
el.parentElement.style.marginLeft = '';
el.parentElement.style.marginRight = '';
el.parentElement.style.borderRadius = '';
el.parentElement.style.boxShadow = '';
}
}
}
el.classList.remove('annotated-block', 'annotated-sub-block', 'has-note', 'cross-block-highlight');
el.removeAttribute('title');
el.removeAttribute('data-annotation-id');
// 移除跨子块标识
const indicator = el.querySelector('.cross-block-indicator');
if (indicator) indicator.remove();
});
// ====== 日志:高亮应用前后子块对比 ======
const afterCleanSubBlocks = __ANNOTATION_DEBUG__ ? containerElement.querySelectorAll('.sub-block') : [];
//console.log(`[BlockHighlighter] 清理后子块数量: ${afterCleanSubBlocks.length}`);
//console.log(`[BlockHighlighter] 清理后子块ID:`, Array.from(afterCleanSubBlocks).map(sb => sb.dataset.subBlockId));
// ====== 新增4.1日志(高亮清理后) ======
try {
const sub41 = containerElement.querySelector('.sub-block[data-sub-block-id="4.1"]');
if (sub41) {
//console.log('[调试][高亮清理后] 4.1 outerHTML:', sub41.outerHTML);
//console.log('[调试][高亮清理后] 4.1 textContent:', sub41.textContent);
} else {
//console.log('[调试][高亮清理后] 4.1 不存在');
}
} catch (e) { console.error('[调试][高亮清理后] 4.1 日志异常', e); }
// ====== 新增:高亮清理后全局检测 ======
if (__ANNOTATION_DEBUG__) {
const allBlocksAfterHighlight = containerElement.querySelectorAll('[data-block-index]');
allBlocksAfterHighlight.forEach(block => {
const subs = block.querySelectorAll('.sub-block');
//console.log(`[检测][高亮清理后] block#${block.dataset.blockIndex} 有${subs.length}个sub-block:`, Array.from(subs).map(sb => (sb.textContent || '').substring(0, 20)));
});
}
// 优先处理子块批注
// 先构建索引,避免每个元素 O(n) 查找
const subBlockMap = new Map();
const blockIndexMap = new Map();
if (Array.isArray(allAnnotations)) {
for (const ann of allAnnotations) {
if (!ann || ann.targetType !== contentIdentifier || !ann.target || !Array.isArray(ann.target.selector)) continue;
const sel = ann.target.selector[0];
if (!sel) continue;
if (sel.subBlockId && (ann.motivation === 'highlighting' || ann.motivation === 'commenting')) {
// 支持同一子块多个注解(尤其是区间标注)
if (!subBlockMap.has(sel.subBlockId)) subBlockMap.set(sel.subBlockId, []);
subBlockMap.get(sel.subBlockId).push(ann);
} else if ((sel.blockIndex !== undefined) && (ann.motivation === 'highlighting' || ann.motivation === 'commenting')) {
const key = String(sel.blockIndex);
if (!blockIndexMap.has(key)) blockIndexMap.set(key, ann);
}
}
}
// 支持真实子块(span)与虚拟子块(段落等被临时标记了 data-sub-block-id)
const subBlockElements = containerElement.querySelectorAll('[data-sub-block-id]');
subBlockElements.forEach((subBlockElement) => {
const subBlockId = subBlockElement.dataset.subBlockId;
if (typeof subBlockId === 'undefined') return;
let annotationsForThis = subBlockMap.get(subBlockId);
if (!annotationsForThis || annotationsForThis.length === 0) {
// fallback: exact 文本匹配(仅在调试或确有 exact 才考虑)
const text = subBlockElement.textContent && subBlockElement.textContent.trim();
if (text) {
const annotation = allAnnotations && allAnnotations.find(ann =>
ann.targetType === contentIdentifier && ann.target && Array.isArray(ann.target.selector) &&
ann.target.selector[0] && ann.target.selector[0].exact &&
text.replace(/\s+/g, '') === ann.target.selector[0].exact.trim().replace(/\s+/g, '') &&
(ann.motivation === 'highlighting' || ann.motivation === 'commenting')
);
if (annotation) {
annotationsForThis = [annotation];
if (__ANNOTATION_DEBUG__) console.warn(`[高亮fallback] subBlockId未命中使用exact文本匹配成功: "${text}"`);
}
}
} else {
// 检查 exact 一致性(仅非区间注解提示);区间注解不依赖 exact
if (annotationsForThis.length === 1) {
const ann = annotationsForThis[0];
const s0 = ann && ann.target && ann.target.selector && ann.target.selector[0];
const isRange = s0 && s0.type === 'SubBlockRangeSelector';
if (!isRange && s0 && s0.exact && subBlockElement.textContent) {
const now = subBlockElement.textContent.trim().replace(/\s+/g, '');
const old = String(s0.exact).trim().replace(/\s+/g, '');
if (now !== old) {
if (__ANNOTATION_DEBUG__) console.warn(`[高亮提示] subBlockId=${subBlockId} 内容与 exact 不一致,已临时纠正 exact 以避免误差`);
try { ann.target.selector[0].exact = subBlockElement.textContent.trim(); } catch { /* noop */ }
}
}
}
}
if (annotationsForThis && annotationsForThis.length) {
annotationsForThis.forEach(ann => applyAnnotationToElement(subBlockElement, ann, contentIdentifier, subBlockId, 'subBlock'));
}
});
// 已移除:整块高亮渲染(统一为跨子块/子块内区间模式)
// ====== 日志:高亮应用后子块对比 ======
const afterHighlightSubBlocks = __ANNOTATION_DEBUG__ ? containerElement.querySelectorAll('.sub-block') : [];
////console.log(`[BlockHighlighter] 高亮后子块数量: ${afterHighlightSubBlocks.length}`);
////console.log(`[BlockHighlighter] 高亮后子块ID:`, Array.from(afterHighlightSubBlocks).map(sb => sb.dataset.subBlockId));
// ====== 新增4.1日志(高亮应用后) ======
try {
const sub41 = containerElement.querySelector('.sub-block[data-sub-block-id="4.1"]');
if (sub41) {
//console.log('[调试][高亮应用后] 4.1 outerHTML:', sub41.outerHTML);
//console.log('[调试][高亮应用后] 4.1 textContent:', sub41.textContent);
} else {
//console.log('[调试][高亮应用后] 4.1 不存在');
}
} catch (e) { console.error('[调试][高亮应用后] 4.1 日志异常', e); }
// ====== 新增:高亮应用后全局检测 ======
if (__ANNOTATION_DEBUG__) {
const allBlocksAfterHighlight = containerElement.querySelectorAll('[data-block-index]');
allBlocksAfterHighlight.forEach(block => {
const subs = block.querySelectorAll('.sub-block');
//console.log(`[检测][高亮应用后] block#${block.dataset.blockIndex} 有${subs.length}个sub-block:`, Array.from(subs).map(sb => (sb.textContent || '').substring(0, 20)));
});
}
// ====== 新增:处理跨子块批注 ======
if (Array.isArray(allAnnotations)) {
let crossBlockAnnotations = allAnnotations.filter(ann =>
ann && ann.isCrossBlock === true &&
ann.targetType === contentIdentifier &&
ann.target && Array.isArray(ann.target.selector) &&
ann.target.selector[0] && ann.target.selector[0].type === 'CrossBlockRangeSelector' &&
Array.isArray(ann.target.selector[0].affectedSubBlocks)
);
// 跨子块去重:按 id 优先,其次按子块集合+偏移
const seenCross = new Set();
const deduped = [];
crossBlockAnnotations.forEach(ann => {
const sel = ann.target.selector[0];
const keyId = ann.id || '';
const keySet = (sel.affectedSubBlocks.slice().sort().join('|') + ':' + (sel.startOffset||'') + ':' + (sel.endOffset||''));
const key = keyId ? ('id:' + keyId) : ('set:' + keySet);
if (!seenCross.has(key)) { seenCross.add(key); deduped.push(ann); }
});
crossBlockAnnotations = deduped;
console.log(`[跨子块高亮] 找到 ${crossBlockAnnotations.length} 个跨子块批注`);
crossBlockAnnotations.forEach(annotation => {
const selector = annotation.target.selector[0];
const affectedSubBlocks = selector.affectedSubBlocks;
console.log(`[跨子块高亮] 处理跨子块批注 ${annotation.id},涉及子块:`, affectedSubBlocks);
// 调用跨子块高亮功能
if (typeof window.applyCrossBlockAnnotation === 'function') {
console.log(`[跨子块高亮] 调用 applyCrossBlockAnnotation参数:`, {
containerElement,
annotation: annotation.id,
contentIdentifier,
affectedSubBlocks
});
window.applyCrossBlockAnnotation(containerElement, annotation, contentIdentifier);
} else {
console.error('[跨子块高亮] applyCrossBlockAnnotation 函数未找到');
}
});
}
// 只在 annotation 没有对应 DOM 时报警
allAnnotations.forEach(ann => {
if (ann.targetType === contentIdentifier && ann.target && ann.target.selector && ann.target.selector[0] && ann.target.selector[0].subBlockId) {
const subBlockId = ann.target.selector[0].subBlockId;
const dom = containerElement.querySelector(`.sub-block[data-sub-block-id=\"${subBlockId}\"]`);
if (!dom) {
console.warn(`[BlockHighlighter] annotation 指向的子块 ${subBlockId} 页面上不存在!`);
}
}
});
performance.mark('applyAnnotations-end');
performance.measure('applyAnnotations', 'applyAnnotations-start', 'applyAnnotations-end');
}
/**
* 辅助函数,将批注样式和事件应用到指定的元素 (块或子块)
* @param {HTMLElement} element - 目标DOM元素
* @param {Object} annotation - 批注对象
* @param {string} contentIdentifier - 内容标识符 ('ocr' 或 'translation')
* @param {string} elementIdentifier - 元素标识符 (blockIndex 或 subBlockId)
* @param {'block'|'subBlock'} elementType - 元素类型
*/
function applyAnnotationToElement(element, annotation, contentIdentifier, elementIdentifier, elementType) {
// ====== 高亮空内容保护 ======
if ((!element.textContent || !element.textContent.trim()) && !element.querySelector('img')) {
console.warn(`[高亮保护] 跳过空内容的${elementType}subBlockId/blockIndex: ${elementIdentifier}annotationId: ${annotation.id}`);
return;
}
// ====== 新增:公式类型检测(仅当元素本身就是公式容器时才走公式高亮) ======
// 子块内含有公式时,不在这里整体套用公式高亮;而是交给后续"局部文本包裹 + 公式外层高亮"的组合逻辑处理。
const isFormulaElement = element.classList && (element.classList.contains('katex') || element.classList.contains('katex-display') || element.classList.contains('katex-inline'));
// 检查是否是跨子块标注的中间子块
const isCrossBlockAnnotation = annotation.target &&
annotation.target.selector &&
annotation.target.selector[0] &&
annotation.target.selector[0].type === 'CrossBlockRangeSelector';
if (isFormulaElement && !isCrossBlockAnnotation) {
// 只有非跨子块标注才走公式专用高亮
const formulaInfo = detectFormulaType(element);
if (formulaInfo && formulaInfo.hasFormula) {
return applyFormulaAnnotation(element, annotation, contentIdentifier, elementIdentifier, elementType, formulaInfo);
}
}
// ====== 高亮前日志 ======
//console.log(`[高亮应用] ${elementType}(${elementIdentifier}) 高亮前内容: "${element.textContent}"`);
// ====== 子块内区间高亮(优先于 exact 文本) ======
if (elementType === 'subBlock' && annotation.target && annotation.target.selector && annotation.target.selector[0]) {
const sel0 = annotation.target.selector[0];
if (sel0.type === 'SubBlockRangeSelector' && (Number.isFinite(sel0.startOffset) || Number.isFinite(sel0.endOffset))) {
const fullLen = (element.textContent || '').length;
const s = Math.max(0, Math.min(Number(sel0.startOffset) || 0, fullLen));
const e = Math.max(0, Math.min(Number(sel0.endOffset) || fullLen, fullLen));
if (e > s) {
applyPartialCrossBlockHighlight(element, annotation, getHighlightColor(annotation.highlightColor || 'yellow'),
(annotation.body && annotation.body[0] && annotation.body[0].value) ? annotation.body[0].value : '',
s, e, 0, 1, false, 'ocr');
return; // 已完成局部高亮
}
}
}
// ====== 精确高亮逻辑,仅对 subBlock 生效 ======
if (elementType === 'subBlock' && annotation.target && annotation.target.selector && annotation.target.selector[0] && annotation.target.selector[0].exact) {
const exact = annotation.target.selector[0].exact.trim();
const elementText = extractTextIgnoringFormulas(element).trim();
console.log('[精确高亮-DOM] 目标文本:', exact);
console.log('[精确高亮-DOM] 元素文本(忽略公式):', elementText);
if (exact && elementText !== exact) {
// 在逻辑文本中查找匹配位置
const idx = elementText.indexOf(exact);
if (idx !== -1) {
console.log('[精确高亮-DOM] 找到匹配位置:', idx, '长度:', exact.length);
// 尝试新的文本搜索方法
const range = createRangeByTextSearch(element, exact);
if (range) {
console.log('[精确高亮-DOM] 文本搜索成功创建Range');
// 应用结构化高亮保持DOM结构
const highlightElement = applyStructuredHighlight(range, annotation, 'exact-highlight');
if (highlightElement) {
// 绑定事件
bindStandardAnnotationEvents(highlightElement, annotation, elementType, elementIdentifier);
console.log('[精确高亮-DOM] 成功应用文本搜索精确高亮');
return;
} else {
console.warn('[精确高亮-DOM] applyStructuredHighlight失败');
}
} else {
console.warn('[精确高亮-DOM] 文本搜索创建Range失败');
}
console.warn('[精确高亮-DOM] 文本搜索方法失败fallback到字符串方法');
// Fallback使用原来的字符串方法
const before = elementText.slice(0, idx);
const match = elementText.slice(idx, idx + exact.length);
const after = elementText.slice(idx + exact.length);
// 构造高亮 span
const highlightSpan = document.createElement('span');
highlightSpan.className = 'exact-highlight annotated-sub-block';
highlightSpan.style.backgroundColor = getHighlightColor(annotation.highlightColor || 'yellow');
highlightSpan.style.borderRadius = '6px';
highlightSpan.style.boxShadow = `0 0 5px ${getHighlightColor(annotation.highlightColor || 'yellow')}`;
highlightSpan.style.padding = '0 3px';
highlightSpan.textContent = match;
if (annotation.body && annotation.body.length > 0 && annotation.body[0].value) {
highlightSpan.title = annotation.body[0].value;
highlightSpan.classList.add('has-note');
}
highlightSpan.dataset.annotationId = annotation.id;
// 事件绑定
if (!highlightSpan._annotationEventBound) {
highlightSpan.addEventListener('click', function handleClick(e) {
e.preventDefault();
e.stopPropagation();
const range = document.createRange();
range.selectNodeContents(this);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
window.globalCurrentSelection = {
text: this.textContent,
range: range.cloneRange(),
annotationId: this.dataset.annotationId,
targetElement: this,
subBlockId: elementIdentifier
};
const parentBlock = this.closest('[data-block-index]');
if (parentBlock) {
window.globalCurrentSelection.blockIndex = parentBlock.dataset.blockIndex;
}
if (typeof window.checkIfTargetIsHighlighted === 'function' &&
typeof window.checkIfTargetHasNote === 'function' &&
typeof window.updateContextMenuOptions === 'function' &&
typeof window.showContextMenu === 'function') {
const isHighlighted = window.checkIfTargetIsHighlighted(annotation.id, contentIdentifier, elementIdentifier, 'subBlockId');
const hasNoteForClick = window.checkIfTargetHasNote(annotation.id, contentIdentifier, elementIdentifier, 'subBlockId');
window.updateContextMenuOptions(isHighlighted, hasNoteForClick);
window.showContextMenu(e.pageX, e.pageY);
}
});
highlightSpan._annotationEventBound = true;
}
// 构造新内容
element.innerHTML = '';
if (before) element.appendChild(document.createTextNode(before));
element.appendChild(highlightSpan);
if (after) element.appendChild(document.createTextNode(after));
// 只做精确高亮,不再整体高亮
return;
} else {
// 没找到 exact降级为整体高亮
console.warn(`[精确高亮] exact 未在 subBlock 中找到,降级为整体高亮: subBlockId=${elementIdentifier}, exact="${exact}", span内容="${elementText}"`);
}
}
}
// 应用标准高亮样式
applyStandardHighlight(element, annotation, elementIdentifier, elementType);
}
// ===== DOM树精确分析工具函数 =====
// 检查节点是否在公式内部
function isInsideFormula(node) {
let current = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
while (current) {
if (current.classList && (
current.classList.contains('katex') ||
current.classList.contains('katex-display') ||
current.classList.contains('katex-inline')
)) {
return true;
}
current = current.parentElement;
}
return false;
}
// 提取元素的纯文本内容(忽略公式)
function extractTextIgnoringFormulas(element) {
let text = '';
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
return isInsideFormula(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while (node = walker.nextNode()) {
text += node.textContent;
}
return text;
}
// 将逻辑文本偏移映射到实际DOM位置改进版处理公式间隙
function mapLogicalToDOM(element, logicalOffset) {
let currentOffset = 0;
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_ALL, // 检查所有节点类型
{
acceptNode: function(node) {
// 文本节点:如果不在公式内,接受
if (node.nodeType === Node.TEXT_NODE) {
return isInsideFormula(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
}
// 元素节点:如果是公式,跳过;否则继续遍历子节点
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList && (
node.classList.contains('katex') ||
node.classList.contains('katex-display') ||
node.classList.contains('katex-inline')
)) {
return NodeFilter.FILTER_REJECT; // 跳过整个公式子树
}
return NodeFilter.FILTER_SKIP; // 继续遍历子节点
}
return NodeFilter.FILTER_REJECT;
}
}
);
let node;
while (node = walker.nextNode()) {
if (node.nodeType === Node.TEXT_NODE) {
const nodeLength = node.textContent.length;
console.log(`[逻辑映射] 检查文本节点: "${node.textContent}" 长度:${nodeLength} 当前偏移:${currentOffset} 目标:${logicalOffset}`);
if (currentOffset + nodeLength >= logicalOffset) {
const result = {
container: node,
offset: logicalOffset - currentOffset
};
console.log(`[逻辑映射] 找到目标位置:`, result);
return result;
}
currentOffset += nodeLength;
}
}
// 如果超出范围,返回最后一个文本节点的末尾
console.warn(`[逻辑映射] 逻辑偏移${logicalOffset}超出范围,当前偏移${currentOffset}`);
// 重新遍历找到最后一个文本节点
const lastWalker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
return isInsideFormula(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
}
}
);
let lastNode = null;
while (node = lastWalker.nextNode()) {
lastNode = node;
}
if (lastNode) {
return {
container: lastNode,
offset: lastNode.textContent.length
};
}
return null;
}
// 创建跨越公式的精确Range新方法直接在DOM中查找文本
function createRangeByTextSearch(element, targetText, startOffset = 0) {
console.log(`[文本搜索Range] 在元素中搜索: "${targetText}" 起始偏移: ${startOffset}`);
// 使用浏览器原生的文本搜索功能
const tempSelection = window.getSelection();
const originalRanges = [];
// 保存当前选择
for (let i = 0; i < tempSelection.rangeCount; i++) {
originalRanges.push(tempSelection.getRangeAt(i).cloneRange());
}
tempSelection.removeAllRanges();
try {
// 创建搜索范围
const searchRange = document.createRange();
searchRange.selectNodeContents(element);
// 搜索目标文本
if (window.find) {
// 使用window.find进行搜索
const found = window.find(targetText, false, false, false, false, true, false);
if (found && tempSelection.rangeCount > 0) {
const foundRange = tempSelection.getRangeAt(0);
console.log('[文本搜索Range] 使用window.find找到文本');
return foundRange.cloneRange();
}
}
// Fallback: 手动搜索
console.log('[文本搜索Range] window.find失败使用手动搜索');
return findTextInDOMRange(element, targetText);
} finally {
// 恢复原始选择
tempSelection.removeAllRanges();
originalRanges.forEach(range => tempSelection.addRange(range));
}
}
// 手动在DOM中搜索文本并返回Range
function findTextInDOMRange(element, targetText) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
return isInsideFormula(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
}
}
);
let fullText = '';
const textNodes = [];
let node;
// 收集所有非公式文本节点
while (node = walker.nextNode()) {
textNodes.push({
node: node,
startOffset: fullText.length,
endOffset: fullText.length + node.textContent.length
});
fullText += node.textContent;
}
console.log(`[手动搜索] 完整文本: "${fullText}"`);
console.log(`[手动搜索] 搜索目标: "${targetText}"`);
// 在完整文本中查找目标
const textIndex = fullText.indexOf(targetText);
if (textIndex === -1) {
console.warn('[手动搜索] 未找到目标文本');
return null;
}
const textEndIndex = textIndex + targetText.length;
console.log(`[手动搜索] 找到文本位置: ${textIndex} - ${textEndIndex}`);
// 找到起始和结束的文本节点
let startNode = null, startOffset = 0;
let endNode = null, endOffset = 0;
for (const nodeInfo of textNodes) {
// 找到起始位置
if (startNode === null && textIndex >= nodeInfo.startOffset && textIndex < nodeInfo.endOffset) {
startNode = nodeInfo.node;
startOffset = textIndex - nodeInfo.startOffset;
console.log(`[手动搜索] 起始节点: "${nodeInfo.node.textContent}" 偏移: ${startOffset}`);
}
// 找到结束位置
if (textEndIndex > nodeInfo.startOffset && textEndIndex <= nodeInfo.endOffset) {
endNode = nodeInfo.node;
endOffset = textEndIndex - nodeInfo.startOffset;
console.log(`[手动搜索] 结束节点: "${nodeInfo.node.textContent}" 偏移: ${endOffset}`);
break;
}
}
if (startNode && endNode) {
const range = document.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
console.log('[手动搜索] 成功创建Range');
return range;
}
console.warn('[手动搜索] 无法创建Range');
return null;
}
// 绑定标准标注事件
function bindStandardAnnotationEvents(element, annotation, elementType, elementIdentifier) {
if (element._annotationEventBound) return;
element.addEventListener('click', function handleClick(e) {
e.preventDefault();
e.stopPropagation();
const range = document.createRange();
range.selectNodeContents(this);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
window.globalCurrentSelection = {
text: this.textContent,
range: range.cloneRange(),
annotationId: this.dataset.annotationId,
targetElement: this,
subBlockId: elementIdentifier
};
const parentBlock = this.closest('[data-block-index]');
if (parentBlock) {
window.globalCurrentSelection.blockIndex = parentBlock.dataset.blockIndex;
}
if (typeof window.checkIfTargetIsHighlighted === 'function' &&
typeof window.checkIfTargetHasNote === 'function' &&
typeof window.updateContextMenuOptions === 'function' &&
typeof window.showContextMenu === 'function') {
const isHighlighted = window.checkIfTargetIsHighlighted(annotation.id, 'ocr', elementIdentifier, 'subBlockId');
const hasNoteForClick = window.checkIfTargetHasNote(annotation.id, 'ocr', elementIdentifier, 'subBlockId');
window.updateContextMenuOptions(isHighlighted, hasNoteForClick);
window.showContextMenu(e.pageX, e.pageY);
}
});
element._annotationEventBound = true;
console.log('[事件绑定] 成功绑定标注事件到元素:', element);
}
// 应用结构化高亮保持DOM结构
function applyStructuredHighlight(range, annotation, className = 'structured-highlight') {
if (!range || range.collapsed) {
console.warn('[结构化高亮] Range无效或为空');
return null;
}
try {
// 创建高亮包装器
const highlightWrapper = document.createElement('span');
highlightWrapper.className = className + ' annotated-sub-block';
highlightWrapper.style.backgroundColor = getHighlightColor(annotation.highlightColor || 'yellow');
highlightWrapper.style.borderRadius = '6px';
highlightWrapper.style.boxShadow = `0 0 5px ${getHighlightColor(annotation.highlightColor || 'yellow')}`;
highlightWrapper.style.padding = '0 3px';
highlightWrapper.dataset.annotationId = annotation.id;
// 添加笔记
if (annotation.body && annotation.body.length > 0 && annotation.body[0].value) {
highlightWrapper.title = annotation.body[0].value;
highlightWrapper.classList.add('has-note');
}
// 提取并包装内容
const contents = range.extractContents();
highlightWrapper.appendChild(contents);
// 插入高亮元素
range.insertNode(highlightWrapper);
console.log('[结构化高亮] 成功应用高亮:', highlightWrapper);
return highlightWrapper;
} catch (e) {
console.error('[结构化高亮] 应用失败:', e);
return null;
}
}
// ===== 新增:标准高亮应用函数 =====
function applyStandardHighlight(element, annotation, elementIdentifier, elementType) {
const originalColor = annotation.highlightColor || 'yellow';
const color = getHighlightColor(originalColor);
const note = annotation.body && annotation.body.length > 0 && annotation.body[0].value ? annotation.body[0].value : '';
// 清除元素上的已有样式
element.style.backgroundColor = '';
element.style.border = '';
element.style.padding = '';
element.style.borderRadius = '';
element.style.boxShadow = '';
const katexDisplayElement = element.classList.contains('katex-display') ? element : element.querySelector('.katex-display');
const imgElement = element.querySelector('img');
let effectiveTableToStyle = null;
if (element.tagName === 'TABLE') {
effectiveTableToStyle = element;
} else if (element.parentElement && element.parentElement.tagName === 'TABLE' &&
element.classList.contains('sub-block') && element.dataset.subBlockId) {
const parentTable = element.parentElement;
const subBlockIdPrefix = element.dataset.subBlockId.split('.')[0];
if (parentTable.dataset.blockIndex === subBlockIdPrefix) {
effectiveTableToStyle = parentTable;
} else {
const tableInsideSpan = element.querySelector('table');
if (tableInsideSpan) effectiveTableToStyle = tableInsideSpan;
}
} else {
const tableInsideElement = element.querySelector('table');
if (tableInsideElement) effectiveTableToStyle = tableInsideElement;
}
if (katexDisplayElement && !effectiveTableToStyle) {
katexDisplayElement.style.border = '2px solid ' + color.replace('0.75)', '1)');
katexDisplayElement.style.borderRadius = '12px';
katexDisplayElement.style.padding = '8px';
katexDisplayElement.style.backgroundColor = color;
katexDisplayElement.style.display = 'block';
katexDisplayElement.style.width = 'fit-content';
katexDisplayElement.style.marginLeft = 'auto';
katexDisplayElement.style.marginRight = 'auto';
katexDisplayElement.style.boxShadow = `0 0 8px ${color}`;
} else if (imgElement && !effectiveTableToStyle) {
element.style.border = '3px solid ' + color.replace('0.75)', '1)');
element.style.borderRadius = '12px';
element.style.padding = '8px';
element.style.backgroundColor = color.replace('0.75)', '0.1)');
element.style.display = 'inline-block';
element.style.boxShadow = `0 0 10px ${color}`;
} else if (effectiveTableToStyle) {
effectiveTableToStyle.style.border = '2px solid ' + color.replace('0.75)', '1)');
effectiveTableToStyle.style.borderRadius = '12px';
effectiveTableToStyle.style.padding = '8px';
effectiveTableToStyle.style.backgroundColor = color.replace('0.75)', '0.1)');
effectiveTableToStyle.style.display = 'block';
effectiveTableToStyle.style.width = 'fit-content';
effectiveTableToStyle.style.marginLeft = 'auto';
effectiveTableToStyle.style.marginRight = 'auto';
} else {
element.style.backgroundColor = color;
element.style.borderRadius = '6px';
element.style.boxShadow = `0 0 5px ${color}`;
element.style.padding = '0 3px';
}
element.classList.add(elementType === 'subBlock' ? 'annotated-sub-block' : 'annotated-block');
if (note) {
element.title = note;
element.classList.add('has-note');
}
element.dataset.annotationId = annotation.id;
// 绑定标准事件
bindStandardAnnotationEvents(element, annotation, elementType, elementIdentifier);
}
// ===== 新增:标准标注事件绑定函数 =====
function bindStandardAnnotationEvents(element, annotation, elementType, elementIdentifier) {
if (!element._annotationEventBound) {
element.addEventListener('click', function handleClick(e) {
e.preventDefault();
e.stopPropagation();
const range = document.createRange();
range.selectNodeContents(this);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
window.globalCurrentSelection = {
text: this.textContent,
range: range.cloneRange(),
annotationId: this.dataset.annotationId,
targetElement: this
};
if (elementType === 'subBlock') {
window.globalCurrentSelection.subBlockId = elementIdentifier;
const parentBlock = this.closest('[data-block-index]');
if (parentBlock) {
window.globalCurrentSelection.blockIndex = parentBlock.dataset.blockIndex;
}
} else {
window.globalCurrentSelection.blockIndex = elementIdentifier;
}
const idToCheck = this.dataset.annotationId;
const identifierForCheck = elementType === 'subBlock' ? window.globalCurrentSelection.subBlockId : window.globalCurrentSelection.blockIndex;
const typeForCheck = elementType === 'subBlock' ? 'subBlockId' : 'blockIndex';
if (typeof window.checkIfTargetIsHighlighted === 'function' &&
typeof window.checkIfTargetHasNote === 'function' &&
typeof window.updateContextMenuOptions === 'function' &&
typeof window.showContextMenu === 'function') {
const isHighlighted = window.checkIfTargetIsHighlighted(idToCheck, window.globalCurrentContentIdentifier, identifierForCheck, typeForCheck);
const hasNoteForClick = window.checkIfTargetHasNote(idToCheck, window.globalCurrentContentIdentifier, identifierForCheck, typeForCheck);
window.updateContextMenuOptions(isHighlighted, hasNoteForClick);
window.showContextMenu(e.pageX, e.pageY);
} else {
console.error("[标注高亮] 上下文菜单相关函数未找到");
}
});
element._annotationEventBound = true;
}
}
// ===== 新增:公式类型检测函数 =====
function detectFormulaType(element) {
const info = {
hasFormula: false,
type: null, // 'block', 'inline', 'mixed'
elements: []
};
const katexDisplay = element.querySelector('.katex-display');
const katexInline = element.querySelector('.katex-inline, .katex:not(.katex-display)');
const hasKatex = element.classList.contains('katex-display') || element.classList.contains('katex');
if (katexDisplay || hasKatex) {
info.hasFormula = true;
info.type = 'block';
info.elements.push(katexDisplay || element);
} else if (katexInline) {
info.hasFormula = true;
info.type = 'inline';
info.elements.push(katexInline);
}
// 检测混合内容(同时有行内和块级公式)
if (katexDisplay && katexInline) {
info.type = 'mixed';
info.elements = [katexDisplay, katexInline];
}
return info;
}
// ===== 新增:公式标注应用函数 =====
function applyFormulaAnnotation(element, annotation, contentIdentifier, elementIdentifier, elementType, formulaInfo) {
const color = getHighlightColor(annotation.highlightColor || 'yellow');
const note = annotation.body && annotation.body.length > 0 && annotation.body[0].value ? annotation.body[0].value : '';
console.log(`[公式高亮] 应用公式标注,类型: ${formulaInfo.type}, 元素: ${elementType}`);
switch (formulaInfo.type) {
case 'block':
applyBlockFormulaHighlight(element, annotation, formulaInfo.elements[0], color, note);
break;
case 'inline':
applyInlineFormulaHighlight(element, annotation, formulaInfo.elements[0], color, note);
break;
case 'mixed':
applyMixedFormulaHighlight(element, annotation, formulaInfo.elements, color, note);
break;
default:
// 降级到标准高亮
applyStandardHighlight(element, annotation, elementIdentifier, elementType);
}
// 添加公式标注标识
element.classList.add('annotated-formula', elementType === 'subBlock' ? 'annotated-sub-block' : 'annotated-block');
element.dataset.annotationId = annotation.id;
if (note) {
element.title = note;
element.classList.add('has-note');
}
// 绑定事件
bindFormulaAnnotationEvents(element, annotation, contentIdentifier, elementIdentifier, elementType);
}
// ===== 新增:块级公式高亮 =====
function applyBlockFormulaHighlight(element, annotation, formulaElement, color, note) {
const targetFormula = formulaElement || element;
// 增强的边框和阴影效果
targetFormula.style.border = `3px solid ${color.replace('0.75)', '1)')}`; // 更不透明的边框
targetFormula.style.borderRadius = '12px';
targetFormula.style.padding = '12px';
targetFormula.style.margin = '8px 0';
targetFormula.style.boxShadow = `0 0 12px ${color.replace('0.75)', '0.4)')}, inset 0 0 0 1px ${color.replace('0.75)', '0.2)')}`; // 内外阴影
targetFormula.style.backgroundColor = color.replace('0.75)', '0.05)'); // 极淡的背景色
// 添加动画效果
targetFormula.style.transition = 'all 0.2s ease';
console.log(`[公式高亮] 块级公式高亮已应用,颜色: ${color}`);
}
// ===== 新增:行内公式高亮 =====
function applyInlineFormulaHighlight(element, annotation, formulaElement, color, note) {
const targetFormula = formulaElement || element;
// 行内公式使用更轻量的样式
targetFormula.style.border = `2px solid ${color.replace('0.75)', '0.8)')}`;
targetFormula.style.borderRadius = '6px';
targetFormula.style.padding = '2px 6px';
targetFormula.style.margin = '0 2px';
targetFormula.style.backgroundColor = color.replace('0.75)', '0.1)');
targetFormula.style.boxShadow = `0 0 4px ${color.replace('0.75)', '0.3)')}`;
console.log(`[公式高亮] 行内公式高亮已应用`);
}
// ===== 新增:混合公式高亮 =====
function applyMixedFormulaHighlight(element, annotation, formulaElements, color, note) {
// 为包含元素添加整体边框
element.style.border = `2px dashed ${color.replace('0.75)', '0.6)')}`;
element.style.borderRadius = '8px';
element.style.padding = '8px';
element.style.backgroundColor = color.replace('0.75)', '0.03)');
// 为每个公式元素添加独立高亮
formulaElements.forEach(formula => {
if (formula.classList.contains('katex-display')) {
applyBlockFormulaHighlight(null, annotation, formula, color, note);
} else {
applyInlineFormulaHighlight(null, annotation, formula, color, note);
}
});
console.log(`[公式高亮] 混合公式高亮已应用,包含 ${formulaElements.length} 个公式`);
}
// ===== 新增:公式标注事件绑定 =====
function bindFormulaAnnotationEvents(element, annotation, contentIdentifier, elementIdentifier, elementType) {
if (!element._annotationEventBound) {
element.addEventListener('click', function handleFormulaClick(e) {
e.preventDefault();
e.stopPropagation();
// 公式选择策略:选择整个包含元素
const range = document.createRange();
range.selectNodeContents(this);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
window.globalCurrentSelection = {
text: this.textContent,
range: range.cloneRange(),
annotationId: this.dataset.annotationId,
targetElement: this,
isFormula: true // 标识这是公式选择
};
if (elementType === 'subBlock') {
window.globalCurrentSelection.subBlockId = elementIdentifier;
const parentBlock = this.closest('[data-block-index]');
if (parentBlock) {
window.globalCurrentSelection.blockIndex = parentBlock.dataset.blockIndex;
}
} else {
window.globalCurrentSelection.blockIndex = elementIdentifier;
}
console.log(`[公式高亮] 公式${elementType}点击,全局选区已设置`);
// 调用上下文菜单
if (typeof window.checkIfTargetIsHighlighted === 'function' &&
typeof window.checkIfTargetHasNote === 'function' &&
typeof window.updateContextMenuOptions === 'function' &&
typeof window.showContextMenu === 'function') {
const idToCheck = this.dataset.annotationId;
const identifierForCheck = elementType === 'subBlock' ? window.globalCurrentSelection.subBlockId : window.globalCurrentSelection.blockIndex;
const typeForCheck = elementType === 'subBlock' ? 'subBlockId' : 'blockIndex';
const isHighlighted = window.checkIfTargetIsHighlighted(idToCheck, contentIdentifier, identifierForCheck, typeForCheck);
const hasNoteForClick = window.checkIfTargetHasNote(idToCheck, contentIdentifier, identifierForCheck, typeForCheck);
window.updateContextMenuOptions(isHighlighted, hasNoteForClick);
window.showContextMenu(e.pageX, e.pageY);
}
});
// 公式悬停效果
element.addEventListener('mouseenter', function() {
this.style.transform = 'scale(1.02)';
this.style.zIndex = '10';
});
element.addEventListener('mouseleave', function() {
this.style.transform = '';
this.style.zIndex = '';
});
element._annotationEventBound = true;
}
}
/**
* 将颜色转换为更浅的荧光版本
* @param {string} color - 原始颜色
* @returns {string} - 转换后的荧光浅色
*/
function getHighlightColor(color) {
// 颜色映射表 - 确保与CSS中的颜色匹配调整亮度平衡
const colorMap = {
'yellow': 'rgba(255, 255, 0, 0.75)', // 增加不透明度
'pink': 'rgba(253, 170, 200, 0.75)', // 增加不透明度
'lightblue': 'rgba(95, 211, 250, 0.75)', // 增加不透明度
'blue': 'rgba(95, 211, 250, 0.75)', // 增加不透明度
'lightgreen': 'rgba(178, 253, 178, 0.75)',// 增加不透明度
'green': 'rgba(178, 253, 178, 0.75)', // 增加不透明度
'purple': 'rgba(221, 160, 221, 0.75)', // 增加不透明度
'orange': 'rgba(255, 165, 0, 0.75)', // 增加不透明度
'red': 'rgba(255, 99, 71, 0.75)', // 增加不透明度
'cyan': 'rgba(0, 204, 204, 0.75)' // 增加不透明度
};
// 如果是常见颜色,使用映射表
if (colorMap[color.toLowerCase()]) {
return colorMap[color.toLowerCase()];
}
// 如果是十六进制颜色转换为半透明的RGBA
if (color.startsWith('#')) {
let r = 0, g = 0, b = 0;
if (color.length === 4) {
// #RGB格式
r = parseInt(color[1] + color[1], 16);
g = parseInt(color[2] + color[2], 16);
b = parseInt(color[3] + color[3], 16);
} else if (color.length === 7) {
// #RRGGBB格式
r = parseInt(color.substring(1, 3), 16);
g = parseInt(color.substring(3, 5), 16);
b = parseInt(color.substring(5, 7), 16);
}
return `rgba(${r}, ${g}, ${b}, 0.75)`;
}
// 如果是RGB或RGBA格式转换为更透明的版本
if (color.startsWith('rgb')) {
if (color.startsWith('rgba')) {
// 已经是RGBA格式调整其不透明度
const rgbaMatch = color.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/);
if (rgbaMatch) {
const [, r, g, b, a] = rgbaMatch;
// 调整不透明度最小0.75
const newAlpha = Math.max(parseFloat(a), 0.75);
return `rgba(${r}, ${g}, ${b}, ${newAlpha})`;
}
} else {
// RGB格式转换为RGBA
const rgbMatch = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
if (rgbMatch) {
const [, r, g, b] = rgbMatch;
return `rgba(${r}, ${g}, ${b}, 0.75)`;
}
}
}
// 默认返回与CSS匹配的黄色高亮
return 'rgba(255, 255, 0, 0.75)';
}
// ========== 新增:单个块/子块高亮/取消高亮 ==========
/**
* 只高亮一个块或子块
* @param {HTMLElement} element - 目标DOM元素
* @param {Object} annotation - 批注对象
* @param {string} contentIdentifier - 内容标识符 ('ocr' 或 'translation')
* @param {string} elementIdentifier - 元素标识符 (blockIndex 或 subBlockId)
* @param {'block'|'subBlock'} elementType - 元素类型
*/
function highlightBlockOrSubBlock(element, annotation, contentIdentifier, elementIdentifier, elementType) {
applyAnnotationToElement(element, annotation, contentIdentifier, elementIdentifier, elementType);
}
/**
* 只移除一个块或子块的高亮
* @param {HTMLElement} element - 目标DOM元素
*/
function removeHighlightFromBlockOrSubBlock(element) {
if (!element) return;
element.style.backgroundColor = '';
element.style.border = '';
element.style.padding = '';
element.style.borderRadius = '';
element.style.boxShadow = '';
element.classList.remove('annotated-block', 'annotated-sub-block', 'has-note', 'has-highlight');
element.removeAttribute('title');
element.removeAttribute('data-annotation-id');
element.removeAttribute('data-highlight-color');
// 还原特殊元素样式(如有)
const katexDisplayElement = element.classList.contains('katex-display') ? element : element.querySelector('.katex-display');
const imgElement = element.querySelector('img');
const tableElement = element.tagName === 'TABLE' ? element : element.querySelector('table');
[katexDisplayElement, imgElement, tableElement].forEach(el => {
if (el) {
el.style.border = '';
el.style.padding = '';
el.style.display = '';
el.style.width = '';
el.style.marginLeft = '';
el.style.marginRight = '';
el.style.borderRadius = '';
el.style.boxShadow = '';
}
});
}
// 暴露新函数
window.applyBlockAnnotations = applyBlockAnnotations;
// 暂时保留旧的函数别名以便兼容 (尽管其内部逻辑已更新)
window.applyParagraphAnnotations = applyBlockAnnotations;
// 移除原始的applyPreprocessedAnnotations (如果存在)
if (window.applyPreprocessedAnnotations) {
delete window.applyPreprocessedAnnotations;
}
// 导出到 window
window.highlightBlockOrSubBlock = highlightBlockOrSubBlock;
window.removeHighlightFromBlockOrSubBlock = removeHighlightFromBlockOrSubBlock;
// ===== 新增:跨子块标注渲染函数 =====
function applyCrossBlockAnnotation(containerElement, annotation, contentIdentifier) {
if (!annotation.target || !annotation.target.selector || !annotation.target.selector[0]) {
console.warn('[跨子块高亮] 标注数据结构不完整:', annotation);
return;
}
const selector = annotation.target.selector[0];
const affectedSubBlocks = selector.affectedSubBlocks || [];
if (affectedSubBlocks.length === 0) {
console.warn('[跨子块高亮] 没有找到影响的子块:', annotation);
return;
}
const __DEBUG__ = (function(){
try { return !!(window && (window.ENABLE_ANNOTATION_DEBUG || localStorage.getItem('ENABLE_ANNOTATION_DEBUG') === 'true')); } catch { return false; }
})();
if (__DEBUG__) console.log(`[跨子块高亮] 渲染跨子块标注,影响 ${affectedSubBlocks.length} 个子块:`, affectedSubBlocks);
const color = getHighlightColor(annotation.highlightColor || 'yellow');
const note = annotation.body && annotation.body.length > 0 && annotation.body[0].value ? annotation.body[0].value : '';
let startId = selector.startSubBlockId || affectedSubBlocks[0];
let endId = selector.endSubBlockId || affectedSubBlocks[affectedSubBlocks.length - 1];
let hasStartOffset = Number.isFinite(selector.startOffset);
let hasEndOffset = Number.isFinite(selector.endOffset);
let startOffset = hasStartOffset ? selector.startOffset : 0;
let endOffset = hasEndOffset ? selector.endOffset : 0;
// 防御性检查如果endOffset为0且不是单字符选择可能有问题
if (hasEndOffset && endOffset === 0 && startId !== endId) {
console.warn(`[跨子块高亮] endOffset为0但这是跨子块选择可能有数据问题`, {
startId, endId, startOffset, endOffset, affectedSubBlocks
});
// 尝试从容器中计算endOffset
const endBlockIndex = endId.split('.')[0];
const endElement = containerElement.querySelector(`[data-sub-block-id="${endId}"]`) ||
containerElement.querySelector(`[data-block-index="${endBlockIndex}"]`);
if (endElement) {
const endText = extractTextIgnoringFormulas(endElement);
console.log(`[跨子块高亮] 尾元素文本长度: ${endText.length}, 设置endOffset为文本长度`);
endOffset = endText.length;
hasEndOffset = true;
}
}
// 可选:使用 exact 精准对齐(若存在)
if (selector.exact && typeof selector.exact === 'string') {
const texts = [];
for (const id of affectedSubBlocks) {
const el = containerElement.querySelector(`[data-sub-block-id="${id}"]`);
texts.push(el ? (el.textContent || '') : '');
}
const combined = texts.join('');
const exact = selector.exact;
// 先尝试直接匹配
let pos = combined.indexOf(exact);
let used = 'direct';
if (pos === -1) {
// 忽略空白匹配
const isWs = (ch) => /[\s\u200B-\u200D\uFEFF]/.test(ch);
const buildNormalized = (s) => {
const norm = [];
const map = [];
for (let i = 0; i < s.length; i++) {
const ch = s[i];
if (!isWs(ch)) { norm.push(ch); map.push(i); }
}
return { norm: norm.join(''), map };
};
const comb = buildNormalized(combined);
const ex = buildNormalized(exact);
const posNorm = comb.norm.indexOf(ex.norm);
if (posNorm !== -1) {
const origStart = comb.map[posNorm];
const origEndExclusive = comb.map[posNorm + ex.norm.length - 1] + 1;
pos = origStart;
used = 'ignore-space';
// 映射为子块与偏移
let acc = 0, startBlockIdx = 0, localStart = 0;
for (let i = 0; i < texts.length; i++) {
const L = texts[i].length;
if (origStart < acc + L) { startBlockIdx = i; localStart = origStart - acc; break; }
acc += L;
}
const endGlobal = origEndExclusive;
acc = 0; let endBlockIdx = texts.length - 1; let localEnd = texts[endBlockIdx].length;
for (let i = 0; i < texts.length; i++) {
const L = texts[i].length;
if (endGlobal <= acc + L) { endBlockIdx = i; localEnd = endGlobal - acc; break; }
acc += L;
}
startId = affectedSubBlocks[startBlockIdx];
endId = affectedSubBlocks[endBlockIdx];
startOffset = localStart;
endOffset = localEnd;
hasStartOffset = true;
hasEndOffset = true;
}
}
if (pos !== -1) {
// 若是直接匹配路径,需要从 pos 反推子块与偏移
if (used === 'direct') {
let acc = 0, startBlockIdx = 0, localStart = 0;
for (let i = 0; i < texts.length; i++) {
const L = texts[i].length;
if (pos < acc + L) { startBlockIdx = i; localStart = pos - acc; break; }
acc += L;
}
const endGlobal = pos + exact.length;
acc = 0; let endBlockIdx = texts.length - 1; let localEnd = texts[endBlockIdx].length;
for (let i = 0; i < texts.length; i++) {
const L = texts[i].length;
if (endGlobal <= acc + L) { endBlockIdx = i; localEnd = endGlobal - acc; break; }
acc += L;
}
startId = affectedSubBlocks[startBlockIdx];
endId = affectedSubBlocks[endBlockIdx];
startOffset = localStart;
endOffset = localEnd;
hasStartOffset = true;
hasEndOffset = true;
}
if (__DEBUG__) console.log(`[跨子块对齐] 使用 exact(${used === 'direct' ? '直接' : '忽略空白'}) 重算偏移:`, { startId, endId, startOffset, endOffset });
}
}
// 只对[startId..endId]之间的子块应用高亮
let startIdxInList = affectedSubBlocks.indexOf(startId);
let endIdxInList = affectedSubBlocks.lastIndexOf(endId);
// 改进:处理 exact 重计算导致的 ID 不匹配问题
if (startIdxInList === -1 || endIdxInList === -1) {
console.warn(`[跨子块高亮] startId(${startId})或endId(${endId})不在affectedSubBlocks中使用全部子块`);
console.warn('[跨子块高亮] 这通常是exact重计算导致的问题');
startIdxInList = 0;
endIdxInList = affectedSubBlocks.length - 1;
}
const effectiveSubBlocks = affectedSubBlocks.slice(startIdxInList, endIdxInList + 1);
if (__DEBUG__) {
console.log('[跨子块调试] effectiveSubBlocks计算:', {
affectedSubBlocks: affectedSubBlocks,
startId: startId,
endId: endId,
startIdxInList: startIdxInList,
endIdxInList: endIdxInList,
effectiveCount: effectiveSubBlocks.length,
effectiveSubBlocks: effectiveSubBlocks
});
}
// 为每个有效子块应用跨子块高亮样式(首尾按偏移做部分高亮)
let firstSubBlock = null;
let lastSubBlock = null;
const createdHighlightElements = [];
effectiveSubBlocks.forEach((subBlockId, index) => {
// 查找子块元素;若不存在,尝试块级元素(虚拟子块)
// 优先匹配真实子块,再回退虚拟子块或块级元素
let subBlockElement = containerElement.querySelector(`.sub-block[data-sub-block-id="${subBlockId}"]`) ||
containerElement.querySelector(`[data-sub-block-id="${subBlockId}"]`);
if (!subBlockElement) {
const blockIndex = subBlockId.split('.')[0];
subBlockElement = containerElement.querySelector(`[data-block-index="${blockIndex}"]`);
if (__DEBUG__) console.log(`[跨子块高亮] 通过块级元素查找: data-block-index="${blockIndex}"`, subBlockElement);
// 如果还是找不到,尝试查找虚拟子块
if (!subBlockElement) {
// 查找具有虚拟子块ID的元素
subBlockElement = containerElement.querySelector(`[data-sub-block-id*="${blockIndex}."]`);
if (subBlockElement && __DEBUG__) console.log(`[跨子块高亮] 通过虚拟子块查找找到:`, subBlockElement);
// 最后尝试:查找任何包含该块索引的元素
if (!subBlockElement) {
const allElements = containerElement.querySelectorAll('[data-block-index], [data-sub-block-id]');
for (const el of allElements) {
const elBlockIndex = el.dataset.blockIndex || el.dataset.subBlockId?.split('.')[0];
if (elBlockIndex === blockIndex) {
subBlockElement = el;
if (__DEBUG__) console.log(`[跨子块高亮] 通过遍历找到匹配元素:`, el);
break;
}
}
}
}
}
if (!subBlockElement) {
console.warn(`[跨子块高亮] 找不到子块元素: ${subBlockId}`);
return;
}
if (index === 0) firstSubBlock = subBlockElement;
if (index === effectiveSubBlocks.length - 1) lastSubBlock = subBlockElement;
const isRealSubBlock = subBlockElement.classList && subBlockElement.classList.contains('sub-block');
if (__DEBUG__ && (subBlockId === startId || subBlockId === endId)) {
const t = subBlockElement.textContent || '';
if (subBlockId === startId && hasStartOffset) {
console.log(`[跨子块调试] 首子块(${startId}) 长度=${t.length}, startOffset=${startOffset}, 边界字符='${t[startOffset]||''}'(#${t.charCodeAt(startOffset)||''})`);
}
if (subBlockId === endId && hasEndOffset) {
console.log(`[跨子块调试] 尾子块(${endId}) 长度=${t.length}, endOffset=${endOffset}, 边界字符='${t[endOffset-1]||''}'(#${t.charCodeAt(endOffset-1)||''})`);
}
}
// 单子块跨块场景:仅在同一子块内局部高亮
if (hasStartOffset && hasEndOffset && startId === endId && subBlockId === startId) {
const created = applyPartialCrossBlockHighlight(subBlockElement, annotation, color, note, startOffset, endOffset, index, effectiveSubBlocks.length, true, contentIdentifier);
if (index === 0 && created) created.id = `ann-${annotation.id}`;
if (created) createdHighlightElements.push(created);
return;
}
// 首子块:从 startOffset 到末尾
if (hasStartOffset && subBlockId === startId) {
const fullLen = (subBlockElement.textContent || '').length;
const safeStart = Math.max(0, Math.min(startOffset, fullLen));
const created = applyPartialCrossBlockHighlight(subBlockElement, annotation, color, note, safeStart, fullLen, index, effectiveSubBlocks.length, true, contentIdentifier);
if (index === 0 && created) created.id = `ann-${annotation.id}`;
if (created) createdHighlightElements.push(created);
return;
}
// 尾子块从0到 endOffset
if (hasEndOffset && subBlockId === endId) {
const fullLen = (subBlockElement.textContent || '').length;
const safeEnd = Math.max(0, Math.min(endOffset, fullLen));
const created = applyPartialCrossBlockHighlight(subBlockElement, annotation, color, note, 0, safeEnd, index, effectiveSubBlocks.length, true, contentIdentifier);
if (index === 0 && created) created.id = `ann-${annotation.id}`;
if (created) createdHighlightElements.push(created);
return;
}
// 中间子块或虚拟子块:整块高亮
const created = applyCrossBlockHighlightStyle(subBlockElement, annotation, color, note, index, effectiveSubBlocks.length);
if (index === 0 && created) created.id = `ann-${annotation.id}`;
if (created) createdHighlightElements.push(created);
});
// 为所有创建的高亮元素绑定事件(任意片段点击都可重新选择整段)
if (createdHighlightElements.length) {
createdHighlightElements.forEach(el => bindCrossBlockAnnotationEvents(el, annotation, contentIdentifier, effectiveSubBlocks));
} else if (firstSubBlock) {
// 兜底:至少绑定在首子块
bindCrossBlockAnnotationEvents(firstSubBlock, annotation, contentIdentifier, effectiveSubBlocks);
}
// 调试自检:拼接已高亮文本与 exact 比对
const __DEBUG__CHECK__ = (function(){
try { return !!(window && (window.ENABLE_ANNOTATION_DEBUG || localStorage.getItem('ENABLE_ANNOTATION_DEBUG') === 'true')); } catch { return false; }
})();
if (__DEBUG__CHECK__ && selector.exact) {
try {
const spans = [];
effectiveSubBlocks.forEach(id => {
const host = containerElement.querySelector(`[data-sub-block-id="${id}"]`) || containerElement.querySelector(`[data-block-index="${String(id).split('.')[0]}"]`);
if (!host) return;
// 包含 host 自身(用于整块高亮的中间段)
if (host.matches && host.matches(`[data-annotation-id="${annotation.id}"]`)) spans.push(host);
const nodes = host.querySelectorAll(`[data-annotation-id="${annotation.id}"]`);
nodes.forEach(n => spans.push(n));
});
const actual = spans.map(s => s.textContent || '').join('');
// 更宽容的文本比较:提取纯文本内容进行比较
const extractPureText = (text) => {
return String(text)
.replace(/[\s\u200B-\u200D\uFEFF]+/g, ' ') // 标准化空白
.replace(/\s+/g, ' ') // 多个空格合并为一个
.trim();
};
const expectedPure = extractPureText(selector.exact);
const actualPure = extractPureText(actual);
const ok = expectedPure === actualPure;
if (!ok) {
console.warn('[跨子块自检] 高亮结果与 exact 不一致', {
annId: annotation.id,
startId, endId, startOffset, endOffset,
expectedPreview: expectedPure.substring(0, 80),
actualPreview: actualPure.substring(0, 80),
expectedLength: expectedPure.length,
actualLength: actualPure.length,
spans: spans.length
});
} else {
console.log('[跨子块自检] 高亮结果与 exact 一致');
}
} catch (e) {
console.warn('[跨子块自检] 检查失败:', e);
}
}
}
// 在子块内部应用"部分"跨子块高亮(使用字符偏移切分)
function applyPartialCrossBlockHighlight(element, annotation, color, note, start, end, position, totalCount, isCrossBlock = true, contentIdentifier = null) {
const text = element.textContent || '';
const isInFormula = (n) => {
let p = n && (n.nodeType === Node.TEXT_NODE ? n.parentElement : n);
while (p) {
if (p.classList && (
p.classList.contains('katex') ||
p.classList.contains('katex-display') ||
p.classList.contains('katex-inline') ||
p.classList.contains('reference-citation') // 保护引用链接
)) return true;
p = p.parentElement;
}
return false;
};
const lenExcludingFormula = (() => {
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
let len = 0, node; while ((node = walker.nextNode())) { if (!isInFormula(node)) len += (node.nodeValue || '').length; }
return len;
})();
let s = Math.max(0, Math.min(start, lenExcludingFormula));
let e = Math.max(0, Math.min(end, lenExcludingFormula));
if (e <= s) return;
// 轻量边界吸附:去除边界处不可见空白,避免看起来“多出一点”
const isWs = (ch) => /[\s\u00A0\u200B-\u200D\uFEFF]/.test(ch);
// 注意s/e 是“忽略公式”的字符坐标,需要在真实文本节点上映射
if (e <= s) return;
// 将“忽略公式”的 s/e 映射到真实文本节点位置(跳过公式)
const mapOffsetToNode = (root, charOffset) => {
let remaining = charOffset;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) {
if (isInFormula(node)) continue;
const l = node.nodeValue ? node.nodeValue.length : 0;
if (remaining <= l) return { node, offset: remaining };
remaining -= l;
}
return null;
};
const startPos = mapOffsetToNode(element, s);
const endPos = mapOffsetToNode(element, e);
if (!startPos || !endPos) return;
// 文本节点内精确包裹(不穿公式)
const wrapTextRange = (node, from, to) => {
if (!node || to <= from) return null;
const full = node.nodeValue || '';
const before = full.slice(0, from);
const mid = full.slice(from, to);
const after = full.slice(to);
const parent = node.parentNode;
if (!parent) return null;
const textBefore = before ? document.createTextNode(before) : null;
const textAfter = after ? document.createTextNode(after) : null;
const span = document.createElement('span');
if (isCrossBlock) {
span.className = 'cross-block-highlight annotated-sub-block';
applyCrossBlockHighlightStyle(span, annotation, color, note, position, totalCount);
} else {
span.className = 'partial-subblock-highlight annotated-sub-block';
span.style.backgroundColor = color;
span.style.borderRadius = '6px';
span.style.boxShadow = `0 0 5px ${color}`;
span.style.padding = '0 3px';
span.dataset.annotationId = annotation.id;
if (note) { span.title = note; span.classList.add('has-note'); }
bindStandardAnnotationEvents(span, annotation, 'subBlock', element.dataset && element.dataset.subBlockId ? element.dataset.subBlockId : undefined);
}
span.textContent = mid;
parent.replaceChild(span, node);
if (textAfter) parent.insertBefore(textAfter, span.nextSibling);
if (textBefore) parent.insertBefore(textBefore, span);
return span;
};
// 对起止节点进行包裹
const createdSpans = [];
if (startPos.node === endPos.node) {
const created = wrapTextRange(startPos.node, startPos.offset, endPos.offset);
if (created) createdSpans.push(created);
} else {
// 修复:在任何替换发生之前,先收集“忽略公式”的所有文本节点,
// 并定位起止节点的索引,避免因替换导致遍历起点丢失。
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
const textNodes = [];
let n;
while ((n = walker.nextNode())) {
if (isInFormula(n)) continue;
textNodes.push(n);
}
const startIndex = textNodes.indexOf(startPos.node);
const endIndex = textNodes.indexOf(endPos.node);
if (startIndex === -1 || endIndex === -1 || startIndex > endIndex) {
// 兜底:若无法定位,保持旧逻辑(至少高亮首尾)
const sNode = startPos.node;
const sLen = (sNode.nodeValue || '').length;
const firstSpan = wrapTextRange(sNode, startPos.offset, sLen);
if (firstSpan) createdSpans.push(firstSpan);
const lastSpan = wrapTextRange(endPos.node, 0, endPos.offset);
if (lastSpan) createdSpans.push(lastSpan);
} else {
// 起点节点from startOffset 到节点末尾
const sNode = textNodes[startIndex];
const sLen = (sNode.nodeValue || '').length;
const firstSpan = wrapTextRange(sNode, startPos.offset, sLen);
if (firstSpan) createdSpans.push(firstSpan);
// 中间文本节点整段包裹startIndex+1 ... endIndex-1
for (let i = startIndex + 1; i < endIndex; i++) {
const midNode = textNodes[i];
const fullLen = (midNode.nodeValue || '').length;
const midSpan = wrapTextRange(midNode, 0, fullLen);
if (midSpan) createdSpans.push(midSpan);
}
// 终点节点:从 0 到 endOffset
const eNode = textNodes[endIndex];
const lastSpan = wrapTextRange(eNode, 0, endPos.offset);
if (lastSpan) createdSpans.push(lastSpan);
}
}
// 精确的公式范围检测:只高亮真正在选择范围内的公式
try {
// 基于DOM位置精确判断公式是否在选择范围内
const isElementInRange = (targetElement) => {
if (!createdSpans.length) return false;
// 获取选择范围的边界
const firstSpan = createdSpans[0];
const lastSpan = createdSpans[createdSpans.length - 1];
// 使用 compareDocumentPosition 进行精确的DOM位置比较
const compareResult1 = targetElement.compareDocumentPosition(firstSpan);
const compareResult2 = targetElement.compareDocumentPosition(lastSpan);
// 检查元素是否在选择范围内
// DOCUMENT_POSITION_FOLLOWING (4): firstSpan在targetElement之后
// DOCUMENT_POSITION_PRECEDING (2): lastSpan在targetElement之前
const afterStart = (compareResult1 & Node.DOCUMENT_POSITION_FOLLOWING) || targetElement === firstSpan || firstSpan.contains(targetElement);
const beforeEnd = (compareResult2 & Node.DOCUMENT_POSITION_PRECEDING) || targetElement === lastSpan || lastSpan.contains(targetElement);
return afterStart && beforeEnd;
};
// 找出真正在范围内的公式
const formulas = element.querySelectorAll('.katex, .katex-display, .katex-inline');
formulas.forEach(formula => {
if (isElementInRange(formula)) {
console.log('[精确公式高亮] 公式在范围内,应用高亮:', formula);
const formulaType = formula.classList.contains('katex-display') ? 'block' : 'inline';
applyFormulaAnnotation(formula, annotation, contentIdentifier, element.dataset?.subBlockId, 'subBlock', {
hasFormula: true,
type: formulaType,
elements: [formula]
});
} else {
console.log('[精确公式高亮] 公式不在范围内,跳过:', formula);
}
});
} catch (e) {
console.warn('[公式高亮] 精确公式范围检测失败:', e);
}
return createdSpans[0] || null;
}
// ===== 新增:跨子块高亮样式应用 =====
function applyCrossBlockHighlightStyle(element, annotation, color, note, position, totalCount) {
// 基础高亮样式
element.style.backgroundColor = color;
// 跨子块高亮不加水平内边距,避免视觉上“吃到”前后字符
element.style.padding = '0';
element.classList.add('cross-block-highlight', 'annotated-sub-block');
element.dataset.annotationId = annotation.id;
if (note) {
element.title = note;
element.classList.add('has-note');
}
// 根据位置应用不同的边框样式,创造连续效果
if (totalCount === 1) {
// 单个子块
element.style.borderRadius = '6px';
element.style.boxShadow = `0 0 5px ${color}`;
} else if (position === 0) {
// 第一个子块
element.style.borderRadius = '6px 0 0 6px';
element.style.boxShadow = `0 0 3px ${color}`;
} else if (position === totalCount - 1) {
// 最后一个子块
element.style.borderRadius = '0 6px 6px 0';
element.style.boxShadow = `0 0 3px ${color}`;
} else {
// 中间的子块
element.style.borderRadius = '0';
element.style.boxShadow = `0 0 2px ${color}`;
}
// 添加跨子块标识
element.style.position = 'relative';
if (position === 0) {
// 只在第一个子块添加标识
const indicator = document.createElement('span');
indicator.className = 'cross-block-indicator';
indicator.style.cssText = `
position: absolute;
top: -8px;
left: -4px;
background: ${color.replace('0.75)', '1)')};
color: white;
font-size: 10px;
padding: 1px 4px;
border-radius: 3px;
font-weight: bold;
pointer-events: none;
z-index: 10;
`;
indicator.textContent = `${totalCount}`;
element.appendChild(indicator);
}
return element;
}
// ===== 新增:跨子块标注事件绑定 =====
function bindCrossBlockAnnotationEvents(element, annotation, contentIdentifier, affectedSubBlocks) {
if (!element._crossBlockAnnotationEventBound) {
element.addEventListener('click', function handleCrossBlockClick(e) {
e.preventDefault();
e.stopPropagation();
// 创建跨子块选区
// 优先使用实际创建的高亮片段作为边界
const allParts = document.querySelectorAll(`[data-annotation-id="${annotation.id}"]\.cross-block-highlight`);
const firstPart = allParts && allParts.length ? allParts[0] : null;
const lastPart = allParts && allParts.length ? allParts[allParts.length - 1] : null;
const firstSubBlock = document.querySelector(`[data-sub-block-id="${affectedSubBlocks[0]}"]`);
const lastSubBlock = document.querySelector(`[data-sub-block-id="${affectedSubBlocks[affectedSubBlocks.length - 1]}"]`);
if ((firstPart && lastPart) || (firstSubBlock && lastSubBlock)) {
const range = document.createRange();
if (firstPart && lastPart) {
range.setStartBefore(firstPart);
range.setEndAfter(lastPart);
} else {
range.setStartBefore(firstSubBlock);
range.setEndAfter(lastSubBlock);
}
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
window.globalCurrentSelection = {
text: selection.toString(),
range: range.cloneRange(),
annotationId: annotation.id,
targetElement: this,
isCrossBlock: true,
affectedSubBlocks: affectedSubBlocks.map(id => ({ subBlockId: id }))
};
console.log(`[跨子块高亮] 跨子块标注点击,已设置全局选区`);
// 调用上下文菜单
if (typeof window.updateCrossBlockContextMenuOptions === 'function' &&
typeof window.showContextMenu === 'function') {
const isHighlighted = true; // 当前已高亮
const hasNote = annotation.body && annotation.body.length > 0 && annotation.body[0].value && annotation.body[0].value.trim() !== '';
window.updateCrossBlockContextMenuOptions(isHighlighted, hasNote);
window.showContextMenu(e.pageX, e.pageY);
}
}
});
element._crossBlockAnnotationEventBound = true;
}
}
// ===== 新增:滚动到批注位置 =====
function scrollToAnnotation(annotationId, smooth = true) {
// 优先锚点元素
let target = document.getElementById(`ann-${annotationId}`);
if (!target) {
// 直接 CSS 选择匹配
try { target = document.querySelector(`[data-annotation-id="${annotationId}"]`); } catch { target = null; }
}
if (!target) {
// 兜底:遍历匹配,避免 selector 因特殊字符失败
const all = document.querySelectorAll('[data-annotation-id]');
for (const el of all) {
if ((el.dataset && el.dataset.annotationId) === String(annotationId)) { target = el; break; }
}
}
if (!target) return false;
const emphasisTarget = (function(el){
try {
const sub = el.closest && el.closest('.sub-block[data-sub-block-id]');
if (sub) return sub;
const blk = el.closest && el.closest('[data-block-index]');
if (blk) return blk;
} catch { /* ignore */ }
return el;
})(target);
const opts = { behavior: smooth ? 'smooth' : 'auto', block: 'center' };
try {
emphasisTarget.scrollIntoView(opts);
// 统一的临时强调
const oldOutline = emphasisTarget.style.outline;
emphasisTarget.classList && emphasisTarget.classList.add('jump-to-highlight-effect');
emphasisTarget.style.outline = '2px solid rgba(59,130,246,0.8)';
setTimeout(() => {
emphasisTarget.style.outline = oldOutline || '';
emphasisTarget.classList && emphasisTarget.classList.remove('jump-to-highlight-effect');
}, 1500);
return true;
} catch { return false; }
}
// 异步等待目标高亮/元素出现后再跳转,解决延迟渲染/分批分块导致的找不到问题
async function scrollToAnnotationAsync(annotationId, options = {}) {
const {
targetType = null, // 'ocr' | 'translation'
subBlockId = null, // 可选:回退匹配用
blockIndex = null, // 可选:回退匹配用
timeoutMs = 4000, // 默认最多等待 4s
pollIntervalMs = 120 // 轮询间隔
} = options;
const deadline = Date.now() + timeoutMs;
let lastError = null;
while (Date.now() < deadline) {
try {
// 1) 优先按 annotationId 精准定位
let target = document.getElementById(`ann-${annotationId}`);
if (!target) {
try { target = document.querySelector(`[data-annotation-id="${annotationId}"]`); } catch { target = null; }
}
if (!target) {
// 兜底:遍历匹配
const allEl = document.querySelectorAll('[data-annotation-id]');
for (const el of allEl) {
if ((el.dataset && el.dataset.annotationId) === String(annotationId)) { target = el; break; }
}
}
if (!target) {
// 2) 回退:在特定内容容器内按 subBlockId/blockIndex 查找
let scope = document;
if (targetType) {
const cid = `${targetType}-content-wrapper`;
scope = document.getElementById(cid) || document;
}
if (subBlockId && !target) {
target = scope.querySelector(`.sub-block[data-sub-block-id="${subBlockId}"]`);
}
if (!target && blockIndex !== null && blockIndex !== undefined && String(blockIndex) !== '') {
target = scope.querySelector(`[data-block-index="${blockIndex}"]`);
}
}
if (target) {
// 将强调目标提升到包含的子块/块,保证滚动锚点与强调元素一致
const emphasisTarget = (function(el){
try {
const sub = el.closest && el.closest('.sub-block[data-sub-block-id]');
if (sub) return sub;
const blk = el.closest && el.closest('[data-block-index]');
if (blk) return blk;
} catch { /* ignore */ }
return el;
})(target);
try {
emphasisTarget.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 闪烁/描边提示
const oldOutline = emphasisTarget.style.outline;
emphasisTarget.classList && emphasisTarget.classList.add('jump-to-highlight-effect');
emphasisTarget.style.outline = '2px solid rgba(59,130,246,0.8)';
setTimeout(() => {
emphasisTarget.style.outline = oldOutline || '';
emphasisTarget.classList && emphasisTarget.classList.remove('jump-to-highlight-effect');
}, 1500);
} catch (_) { /* ignore */ }
return true;
}
} catch (e) {
lastError = e;
}
// 等待下一次轮询
await new Promise(r => setTimeout(r, pollIntervalMs));
}
if (lastError) console.warn('[scrollToAnnotationAsync] 跳转失败可能超时或DOM未准备好:', lastError);
return false;
}
// 暴露跨子块高亮功能
window.applyCrossBlockAnnotation = applyCrossBlockAnnotation;
window.applyCrossBlockHighlightStyle = applyCrossBlockHighlightStyle;
window.bindCrossBlockAnnotationEvents = bindCrossBlockAnnotationEvents;
window.scrollToAnnotation = scrollToAnnotation;
window.scrollToAnnotationAsync = scrollToAnnotationAsync;
// 暴露公式相关功能
window.detectFormulaType = detectFormulaType;
window.applyFormulaAnnotation = applyFormulaAnnotation;