From becd567b3c89e2c233d7e9dadf00bf6452ec7736 Mon Sep 17 00:00:00 2001 From: "Austin (Ngoc Thang) Pham" Date: Tue, 14 Jul 2026 13:35:53 +1000 Subject: [PATCH] fix(client): skip structural whitespace in annotations Copilot-Session: 7a66568e-3c1d-4cc8-8f79-acfc0b6c3f91 Signed-off-by: Austin (Ngoc Thang) Pham --- skills/htmlit/client/annotate.js | 6 +- tests/client/fixtures/selector-collision.html | 4 ++ tests/client/selector-anchor.test.mjs | 55 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/skills/htmlit/client/annotate.js b/skills/htmlit/client/annotate.js index aafd553..f16eb2f 100644 --- a/skills/htmlit/client/annotate.js +++ b/skills/htmlit/client/annotate.js @@ -213,7 +213,9 @@ export function wrapRange(range, id, cls, attr, title) { if (!root) return; var segs = []; if (range.startContainer === range.endContainer && range.startContainer.nodeType === 3) { - segs.push({ node: range.startContainer, from: range.startOffset, to: range.endOffset }); + if (/\S/.test(range.startContainer.nodeValue.slice(range.startOffset, range.endOffset))) { + segs.push({ node: range.startContainer, from: range.startOffset, to: range.endOffset }); + } } else { var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); var n; @@ -224,7 +226,7 @@ export function wrapRange(range, id, cls, attr, title) { if (!intersects) continue; var from = (n === range.startContainer) ? range.startOffset : 0; var to = (n === range.endContainer) ? range.endOffset : n.nodeValue.length; - if (to > from) segs.push({ node: n, from: from, to: to }); + if (to > from && /\S/.test(n.nodeValue.slice(from, to))) segs.push({ node: n, from: from, to: to }); } } // wrap last-to-first so earlier segments' offsets stay valid as nodes split diff --git a/tests/client/fixtures/selector-collision.html b/tests/client/fixtures/selector-collision.html index 98eb20d..42a5422 100644 --- a/tests/client/fixtures/selector-collision.html +++ b/tests/client/fixtures/selector-collision.html @@ -3,6 +3,10 @@ Selector collision +
diff --git a/tests/client/selector-anchor.test.mjs b/tests/client/selector-anchor.test.mjs index c12270c..1f46742 100644 --- a/tests/client/selector-anchor.test.mjs +++ b/tests/client/selector-anchor.test.mjs @@ -95,3 +95,58 @@ test("selecting a nested inline element opens the annotation menu despite a deco if (review) await review.stop(); } }); + +test("comment anchors do not turn structural whitespace into grid children", async (t) => { + if (state.unavailable) return t.skip(state.unavailable); + let review, page; + try { + review = await startReview(FIXTURE); + page = await state.browser.newPage(); + await page.setViewport({ width: 1440, height: 1000 }); + await page.goto(review.url, { waitUntil: "domcontentloaded" }); + + const range = await page.evaluate(() => { + const grid = document.querySelector(".grid"); + const card = grid.querySelector(".card"); + const selected = card.textContent; + const start = grid.textContent.indexOf(selected); + return { + container: ".grid", + start, + end: start + selected.length, + text: selected.replace(/\s+/g, " ").trim(), + }; + }); + assert.notEqual(range.start, -1); + + await fetch(`${review.base}/api/${review.key}/highlights`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ highlights: [{ + id: "cm-grid", kind: "comment", prompt: "Explain this card", comments: ["Explain this card"], + text: "", range, + }] }), + }); + await page.reload({ waitUntil: "domcontentloaded" }); + await page.waitForFunction(() => document.querySelector('mark.htmlit-cm[data-htmlit-cm="cm-grid"]'), { timeout: 20000 }); + + const result = await page.evaluate(() => { + const grid = document.querySelector(".grid"); + const marks = [...document.querySelectorAll('mark.htmlit-cm[data-htmlit-cm="cm-grid"]')]; + return { + markCount: marks.length, + whitespaceMarks: marks.filter((mark) => !mark.textContent.trim()).length, + directGridMarks: grid.querySelectorAll(':scope > mark.htmlit-cm[data-htmlit-cm="cm-grid"]').length, + gridChildren: grid.children.length, + }; + }); + + assert.ok(result.markCount > 0, "the selected text should still have comment anchors"); + assert.equal(result.whitespaceMarks, 0, "structural whitespace should not be wrapped in marks"); + assert.equal(result.directGridMarks, 0, "the grid should not gain a synthetic mark child"); + assert.equal(result.gridChildren, 1, "the grid should keep only its artifact card"); + } finally { + if (page) await page.close(); + if (review) await review.stop(); + } +});