| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| /* eslint-disable @devtools/enforce-custom-element-definitions-location */ |
| |
| import * as TextUtils from '../../../models/text_utils/text_utils.js'; |
| |
| import {HighlightManager} from './HighlightManager.js'; |
| import {type HighlightChange, highlightRangesWithStyleClass, revertDomChanges} from './MarkupHighlight.js'; |
| |
| export class HighlightElement extends HTMLElement { |
| static readonly observedAttributes = ['ranges', 'current-range', 'type']; |
| #ranges: TextUtils.TextRange.SourceRange[] = []; |
| #currentRange: TextUtils.TextRange.SourceRange|undefined; |
| #type = 'css'; |
| #markupChanges: HighlightChange[] = []; |
| |
| attributeChangedCallback(name: string, oldValue: string|null, newValue: string|null): void { |
| if (oldValue === newValue) { |
| return; |
| } |
| switch (name) { |
| case 'ranges': |
| this.#ranges = parseRanges(newValue); |
| break; |
| case 'current-range': |
| this.#currentRange = parseRanges(newValue)[0]; |
| break; |
| case 'type': |
| this.#type = newValue || 'css'; |
| break; |
| } |
| queueMicrotask(() => { |
| if (this.#type === 'css') { |
| HighlightManager.instance().set(this, this.#ranges, this.#currentRange); |
| } else { |
| revertDomChanges(this.#markupChanges); |
| highlightRangesWithStyleClass(this, this.#ranges, 'highlight', this.#markupChanges); |
| } |
| }); |
| } |
| } |
| |
| function parseRanges(value: string|null): TextUtils.TextRange.SourceRange[] { |
| if (!value) { |
| return []; |
| } |
| const ranges = value.split(' ') |
| .filter(rangeString => { |
| const parts = rangeString.split(','); |
| // A valid range string must have exactly two parts. |
| if (parts.length !== 2) { |
| return false; |
| } |
| // Both parts must be convertible to valid numbers. |
| const num1 = Number(parts[0]); |
| const num2 = Number(parts[1]); |
| return !isNaN(num1) && !isNaN(num2); |
| }) |
| .map(rangeString => { |
| const parts = rangeString.split(',').map(part => Number(part)); |
| return new TextUtils.TextRange.SourceRange(parts[0], parts[1]); |
| }); |
| return sortAndMergeRanges(ranges); |
| } |
| |
| function sortAndMergeRanges(ranges: TextUtils.TextRange.SourceRange[]): TextUtils.TextRange.SourceRange[] { |
| // Sort by start position. |
| ranges.sort((a, b) => a.offset - b.offset); |
| |
| if (ranges.length === 0) { |
| return []; |
| } |
| |
| // Merge overlapping ranges. |
| const merged = [ranges[0]]; |
| for (let i = 1; i < ranges.length; i++) { |
| const last = merged[merged.length - 1]; |
| const current = ranges[i]; |
| if (current.offset <= last.offset + last.length) { |
| const newEnd = Math.max(last.offset + last.length, current.offset + current.length); |
| const newLength = newEnd - last.offset; |
| merged[merged.length - 1] = new TextUtils.TextRange.SourceRange(last.offset, newLength); |
| } else { |
| merged.push(current); |
| } |
| } |
| return merged; |
| } |
| |
| customElements.define('devtools-highlight', HighlightElement); |