| |
| |
| |
|
|
| import {HighlightStyle, RectCacheEntry} from "./types"; |
| import {HIGHLIGHT_CONSTANTS} from "./constants"; |
|
|
| |
| export type CharIntervalUnderlineSeg = { x1: number; x2: number; y: number }; |
|
|
| export class HighlightManager { |
| private rectCache: Map<string, RectCacheEntry>; |
| private underlineCache: Map<string, SVGLineElement>; |
| private svgOverlay: SVGSVGElement; |
| private currentStyle: HighlightStyle = 'border'; |
| private readonly MAX_RETRY_ATTEMPTS = 10; |
| |
| private rectCacheWait = { indices: 0, interval: 0 }; |
| private intervalUnderlineLines: SVGLineElement[] = []; |
|
|
| constructor( |
| svgOverlay: SVGSVGElement, |
| rectCache: Map<string, RectCacheEntry>, |
| underlineCache: Map<string, SVGLineElement> |
| ) { |
| this.svgOverlay = svgOverlay; |
| this.rectCache = rectCache; |
| this.underlineCache = underlineCache; |
| } |
|
|
| |
| |
| |
| |
| |
| setHighlightedIndices(indices: Set<number>, highlightStyle: HighlightStyle = 'border'): void { |
| this.whenRectCacheReady('indices', 'HighlightManager: token 高亮缓存未就绪,已达最大重试', () => { |
| this.removeIntervalSvgLines(); |
|
|
| if (this.currentStyle !== highlightStyle) { |
| this.clearPreviousStyle(this.currentStyle); |
| } |
| this.currentStyle = highlightStyle; |
|
|
| const highlightedRects: SVGRectElement[] = []; |
|
|
| this.rectCache.forEach(({ rect, tokenIndex }, rectKey) => { |
| const currentClass = rect.getAttribute('class') || ''; |
| const isHighlighted = indices.has(tokenIndex); |
|
|
| if (highlightStyle === 'underline') { |
| this.applyUnderlineHighlight(rect, rectKey, isHighlighted, currentClass); |
| } else { |
| this.applyBorderHighlight(rect, rectKey, isHighlighted, currentClass, highlightedRects); |
| } |
| }); |
|
|
| if (highlightStyle === 'border' && highlightedRects.length > 0) { |
| highlightedRects.forEach(rect => { |
| if (rect.parentNode === this.svgOverlay) { |
| this.svgOverlay.appendChild(rect); |
| } |
| }); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| private applyUnderlineHighlight( |
| rect: SVGRectElement, |
| rectKey: string, |
| isHighlighted: boolean, |
| currentClass: string |
| ): void { |
| if (isHighlighted) { |
| |
| this.removeBorderStyle(rect); |
|
|
| |
| let underline = this.underlineCache.get(rectKey); |
| if (!underline) { |
| underline = this.createUnderline(rectKey); |
| } |
|
|
| |
| this.updateUnderlinePosition(rect, underline); |
|
|
| |
| this.updateHighlightClass(rect, true); |
| } else { |
| |
| const underline = this.underlineCache.get(rectKey); |
| if (underline) { |
| underline.style.display = 'none'; |
| } |
|
|
| |
| this.removeBorderStyle(rect); |
|
|
| |
| this.updateHighlightClass(rect, false); |
| } |
| } |
|
|
| |
| |
| |
| private applyBorderHighlight( |
| rect: SVGRectElement, |
| rectKey: string, |
| isHighlighted: boolean, |
| currentClass: string, |
| highlightedRects: SVGRectElement[] |
| ): void { |
| if (isHighlighted) { |
| |
| const underline = this.underlineCache.get(rectKey); |
| if (underline) { |
| underline.style.display = 'none'; |
| } |
|
|
| |
| rect.setAttribute('stroke', HIGHLIGHT_CONSTANTS.HIGHLIGHT_COLOR); |
| rect.setAttribute('stroke-width', HIGHLIGHT_CONSTANTS.BORDER_WIDTH); |
| rect.setAttribute('stroke-opacity', '1'); |
| rect.removeAttribute('stroke-dasharray'); |
|
|
| |
| this.updateHighlightClass(rect, true); |
|
|
| |
| highlightedRects.push(rect); |
| } else { |
| |
| this.removeBorderStyle(rect); |
|
|
| |
| const underline = this.underlineCache.get(rectKey); |
| if (underline) { |
| underline.style.display = 'none'; |
| } |
|
|
| |
| this.updateHighlightClass(rect, false); |
| } |
| } |
|
|
| |
| |
| |
| clearHighlight(): void { |
| this.clearRectHighlightsOnly(); |
| this.removeIntervalSvgLines(); |
| } |
|
|
| |
| private clearRectHighlightsOnly(): void { |
| this.rectCache.forEach(({ rect }, rectKey) => { |
| this.removeBorderStyle(rect); |
| this.updateHighlightClass(rect, false); |
| const underline = this.underlineCache.get(rectKey); |
| if (underline) { |
| underline.style.display = 'none'; |
| } |
| }); |
| } |
|
|
| private removeIntervalSvgLines(): void { |
| for (const line of this.intervalUnderlineLines) { |
| line.remove(); |
| } |
| this.intervalUnderlineLines = []; |
| } |
|
|
| |
| clearCharIntervalUnderlines(): void { |
| this.removeIntervalSvgLines(); |
| } |
|
|
| |
| |
| |
| setCharIntervalUnderlines(segments: CharIntervalUnderlineSeg[]): void { |
| this.whenRectCacheReady('interval', 'HighlightManager: 区间下划线缓存未就绪,已达最大重试', () => { |
| this.clearRectHighlightsOnly(); |
| this.removeIntervalSvgLines(); |
| this.appendIntervalUnderlineLines(segments); |
| }); |
| } |
|
|
| |
| updateCharIntervalUnderlines(segments: CharIntervalUnderlineSeg[]): void { |
| this.removeIntervalSvgLines(); |
| this.appendIntervalUnderlineLines(segments); |
| } |
|
|
| |
| |
| |
| private whenRectCacheReady( |
| slot: 'indices' | 'interval', |
| warnMessage: string, |
| run: () => void |
| ): void { |
| if (this.rectCache.size > 0) { |
| this.rectCacheWait[slot] = 0; |
| run(); |
| return; |
| } |
| if (this.rectCacheWait[slot] < this.MAX_RETRY_ATTEMPTS) { |
| this.rectCacheWait[slot] += 1; |
| setTimeout(() => this.whenRectCacheReady(slot, warnMessage, run), 50); |
| return; |
| } |
| console.warn(warnMessage); |
| this.rectCacheWait[slot] = 0; |
| } |
|
|
| private static underlineStrokeAttrs(line: SVGLineElement, cssClass: string): void { |
| line.setAttribute('stroke', HIGHLIGHT_CONSTANTS.HIGHLIGHT_COLOR); |
| line.setAttribute('stroke-width', HIGHLIGHT_CONSTANTS.UNDERLINE_WIDTH); |
| line.setAttribute('stroke-opacity', '1'); |
| line.setAttribute('class', cssClass); |
| } |
|
|
| private appendIntervalUnderlineLines(segments: CharIntervalUnderlineSeg[]): void { |
| const ns = 'http://www.w3.org/2000/svg'; |
| for (const s of segments) { |
| const line = document.createElementNS(ns, 'line'); |
| HighlightManager.underlineStrokeAttrs(line, HIGHLIGHT_CONSTANTS.INTERVAL_UNDERLINE_CLASS); |
| line.setAttribute('x1', String(s.x1)); |
| line.setAttribute('x2', String(s.x2)); |
| line.setAttribute('y1', String(s.y)); |
| line.setAttribute('y2', String(s.y)); |
| this.svgOverlay.appendChild(line); |
| this.intervalUnderlineLines.push(line); |
| } |
| } |
|
|
| |
| |
| |
| private clearPreviousStyle(previousStyle: HighlightStyle): void { |
| if (previousStyle === 'underline') { |
| |
| this.underlineCache.forEach((underline) => { |
| underline.style.display = 'none'; |
| }); |
| } else if (previousStyle === 'border') { |
| |
| this.rectCache.forEach(({ rect }) => { |
| this.removeBorderStyle(rect); |
| }); |
| } |
| } |
|
|
| |
| |
| |
| private removeBorderStyle(rect: SVGRectElement): void { |
| rect.removeAttribute('stroke'); |
| rect.removeAttribute('stroke-width'); |
| rect.removeAttribute('stroke-opacity'); |
| rect.removeAttribute('stroke-dasharray'); |
| } |
|
|
| |
| |
| |
| private createUnderline(rectKey: string): SVGLineElement { |
| const underline = document.createElementNS('http://www.w3.org/2000/svg', 'line'); |
| HighlightManager.underlineStrokeAttrs(underline, HIGHLIGHT_CONSTANTS.UNDERLINE_CLASS); |
| this.svgOverlay.appendChild(underline); |
| this.underlineCache.set(rectKey, underline); |
| return underline; |
| } |
|
|
| |
| |
| |
| private updateHighlightClass(rect: SVGRectElement, isHighlighted: boolean): void { |
| const currentClass = rect.getAttribute('class') || ''; |
| if (isHighlighted) { |
| if (!currentClass.includes(HIGHLIGHT_CONSTANTS.HIGHLIGHT_CLASS)) { |
| rect.setAttribute('class', (currentClass + ' ' + HIGHLIGHT_CONSTANTS.HIGHLIGHT_CLASS).trim()); |
| } |
| } else { |
| const newClass = currentClass.replace(new RegExp(`\\b${HIGHLIGHT_CONSTANTS.HIGHLIGHT_CLASS}\\b`, 'g'), '').trim(); |
| rect.setAttribute('class', newClass); |
| } |
| } |
|
|
| |
| |
| |
| private updateUnderlinePosition(rect: SVGRectElement, underline: SVGLineElement): void { |
| const x = parseFloat(rect.getAttribute('x') || '0'); |
| const y = parseFloat(rect.getAttribute('y') || '0'); |
| const width = parseFloat(rect.getAttribute('width') || '0'); |
| const height = parseFloat(rect.getAttribute('height') || '0'); |
| const bottomY = y + height; |
|
|
| underline.setAttribute('x1', x.toString()); |
| underline.setAttribute('x2', (x + width).toString()); |
| underline.setAttribute('y1', bottomY.toString()); |
| underline.setAttribute('y2', bottomY.toString()); |
| underline.style.display = ''; |
| } |
|
|
| |
| |
| |
| updateUnderlinePositions(): void { |
| this.rectCache.forEach(({ rect }, rectKey) => { |
| const underline = this.underlineCache.get(rectKey); |
| if (underline && underline.style.display !== 'none') { |
| this.updateUnderlinePosition(rect, underline); |
| } |
| }); |
| } |
| } |
|
|
|
|