fix(sidebar): highlights with math split in read-only text

This commit is contained in:
Elian Doran
2026-04-11 14:28:58 +03:00
parent 2d6f1ee9b7
commit 4ffa016045
2 changed files with 74 additions and 56 deletions

View File

@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { extractHighlightsFromStaticHtml } from "./HighlightsList.js";
describe("extractHighlightsFromStaticHtml", () => {
it("extracts highlighted text with math equations", () => {
it("extracts a single highlight containing text and math equation together", () => {
const container = document.createElement("div");
container.innerHTML = `<p>
<span style="background-color:hsl(30,75%,60%);">
@@ -17,15 +17,35 @@ describe("extractHighlightsFromStaticHtml", () => {
const highlights = extractHighlightsFromStaticHtml(container);
// Should extract 3 highlights: "Highlighted ", the math element, and " math"
expect(highlights.length).toBe(3);
// Should extract 1 combined highlight, not 3 separate ones
expect(highlights.length).toBe(1);
// The math highlight should preserve the .math-tex wrapper
const mathHighlight = highlights.find(h => h.text.includes("math-tex"));
expect(mathHighlight).toBeDefined();
expect(mathHighlight?.text).toContain('class="math-tex"');
expect(mathHighlight?.text).toContain("e=mc^2");
expect(mathHighlight?.attrs.background).toBeTruthy();
// The highlight should contain the full innerHTML of the styled span
const highlight = highlights[0];
expect(highlight.text).toContain("Highlighted");
expect(highlight.text).toContain("math-tex");
expect(highlight.text).toContain("e=mc^2");
expect(highlight.text).toContain("math");
expect(highlight.attrs.background).toBeTruthy();
document.body.removeChild(container);
});
it("extracts separate highlights for differently styled spans", () => {
const container = document.createElement("div");
container.innerHTML = `<p>
<span style="background-color:yellow;">Yellow text</span>
normal text
<span style="background-color:red;">Red text</span>
</p>`;
document.body.appendChild(container);
const highlights = extractHighlightsFromStaticHtml(container);
// Should extract 2 separate highlights (yellow and red)
expect(highlights.length).toBe(2);
expect(highlights[0].text).toBe("Yellow text");
expect(highlights[1].text).toBe("Red text");
document.body.removeChild(container);
});

View File

@@ -270,64 +270,62 @@ function ReadOnlyTextHighlightsList() {
export function extractHighlightsFromStaticHtml(el: HTMLElement | null) {
if (!el) return [];
const { color: defaultColor, backgroundColor: defaultBackgroundColor } = getComputedStyle(el);
const walker = document.createTreeWalker(
el,
NodeFilter.SHOW_TEXT,
null
);
const highlights: DomHighlight[] = [];
const processedMathElements = new Set<Element>();
const processedElements = new Set<Element>();
let node: Node | null;
while ((node = walker.nextNode())) {
const el = node.parentElement;
if (!el || !node.textContent?.trim()) continue;
// Find all elements with inline background-color or color styles
const styledElements = el.querySelectorAll<HTMLElement>('[style*="background-color"], [style*="color"]');
const style = getComputedStyle(el);
for (const styledEl of styledElements) {
if (processedElements.has(styledEl)) continue;
if (!styledEl.textContent?.trim()) continue;
// For elements inside math-tex, get styles from the styled ancestor
const mathEl = el.closest(".math-tex");
const attrs: RawHighlight["attrs"] = {
bold: !!styledEl.closest("strong"),
italic: !!styledEl.closest("em"),
underline: !!styledEl.closest("u"),
background: styledEl.style.backgroundColor,
color: styledEl.style.color
};
// Skip if we've already processed this math element
if (mathEl && processedMathElements.has(mathEl)) continue;
if (Object.values(attrs).some(Boolean)) {
processedElements.add(styledEl);
const styledEl = mathEl?.parentElement ?? el;
const styledElStyle = styledEl !== el ? getComputedStyle(styledEl) : style;
highlights.push({
id: randomString(),
text: styledEl.innerHTML,
element: styledEl,
attrs
});
}
}
if (
el.closest('strong, em, u') ||
style.color !== defaultColor ||
style.backgroundColor !== defaultBackgroundColor ||
styledElStyle.color !== defaultColor ||
styledElStyle.backgroundColor !== defaultBackgroundColor
) {
// Also find bold, italic, underline elements
const formattingElements = el.querySelectorAll<HTMLElement>("strong, em, u, b, i");
const attrs: RawHighlight["attrs"] = {
bold: !!el.closest("strong"),
italic: !!el.closest("em"),
underline: !!el.closest("u"),
background: styledEl.style.backgroundColor,
color: styledEl.style.color
};
for (const formattedEl of formattingElements) {
// Skip if already processed or inside a processed element
if (processedElements.has(formattedEl)) continue;
if (Array.from(processedElements).some(processed => processed.contains(formattedEl))) continue;
if (!formattedEl.textContent?.trim()) continue;
if (Object.values(attrs).some(Boolean)) {
const text = mathEl ? mathEl.outerHTML : node.textContent;
const attrs: RawHighlight["attrs"] = {
bold: formattedEl.matches("strong, b"),
italic: formattedEl.matches("em, i"),
underline: formattedEl.matches("u"),
background: formattedEl.style.backgroundColor,
color: formattedEl.style.color
};
// Track processed math elements to avoid duplicates
if (mathEl) {
processedMathElements.add(mathEl);
}
if (Object.values(attrs).some(Boolean)) {
processedElements.add(formattedEl);
highlights.push({
id: randomString(),
text,
element: el,
attrs
});
}
highlights.push({
id: randomString(),
text: formattedEl.innerHTML,
element: formattedEl,
attrs
});
}
}