Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions skills/htmlit/client/annotate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions tests/client/fixtures/selector-collision.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<head>
<meta charset="utf-8" />
<title>Selector collision</title>
<style>
.grid { display: grid; grid-template-columns: minmax(0, 1fr); }
.grid > * { min-width: 0; }
</style>
</head>
<body>
<div class="layout">
Expand Down
55 changes: 55 additions & 0 deletions tests/client/selector-anchor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
Loading