Skip to content

Commit 7515c9a

Browse files
committed
Support annotations across readable content
1 parent 9ffa9df commit 7515c9a

2 files changed

Lines changed: 128 additions & 36 deletions

File tree

assets/site.js

Lines changed: 126 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const REVIEW_OFFSETS = REVIEW_INTERVALS.reduce((offsets, interval) => {
66
return offsets;
77
}, []);
88
const PLAN_DAY_COUNT = CHAPTER_COUNT + REVIEW_OFFSETS.at(-1);
9-
const ANNOTATABLE_SELECTOR = "h2, h3, h4, p, li, blockquote, td, th";
9+
const ANNOTATABLE_SELECTOR = "h1, h2, h3, h4, h5, h6, p, li, blockquote, td, th, pre";
1010
const DISCUSSION_NEW_URL = "https://github.com/Lling0000/Vibe_coding_guide/discussions/new?category=q-a";
1111
const PDF_EXPORT_SCRIPTS = [
1212
"https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js",
@@ -361,7 +361,7 @@ const copy = {
361361
annotationEmpty: "当前阅读范围还没有批注。",
362362
annotationNeedSelection: "先在正文里选中一段文字。",
363363
annotationOutside: "请选中正文里的文字。",
364-
annotationSameBlock: "一次批注先选同一段落、标题或列表项里的文字。",
364+
annotationSameBlock: "这段选择暂时无法转换成批注,请少选一点,或避开图表区域。",
365365
annotationCommentPrompt: "给这段文字添加评论:",
366366
annotationNoComment: "没有评论",
367367
annotationSaved: "批注已保存。",
@@ -473,7 +473,7 @@ const copy = {
473473
annotationEmpty: "No annotations in the current reading range yet.",
474474
annotationNeedSelection: "Select text in the guide first.",
475475
annotationOutside: "Please select text inside the guide.",
476-
annotationSameBlock: "For now, keep one annotation inside the same paragraph, heading, or list item.",
476+
annotationSameBlock: "This selection cannot be annotated yet. Try a shorter selection or avoid diagram areas.",
477477
annotationCommentPrompt: "Add a comment for this text:",
478478
annotationNoComment: "No comment",
479479
annotationSaved: "Annotation saved.",
@@ -1262,7 +1262,7 @@ function positionAnnotationPopover(range) {
12621262

12631263
els.annotationPopover.classList.toggle("is-below", shouldPlaceBelow);
12641264
els.annotationPopover.style.left = `${left}px`;
1265-
els.annotationPopover.style.top = `${Math.max(inset, top)}px`;
1265+
els.annotationPopover.style.top = `${Math.min(window.innerHeight - inset, Math.max(inset, top))}px`;
12661266
}
12671267

12681268
function updateAnnotationPopoverFromSelection() {
@@ -1295,6 +1295,95 @@ function annotationRangeForAction() {
12951295
return state.annotationRange ? state.annotationRange.cloneRange() : null;
12961296
}
12971297

1298+
function rangeIntersectsNode(range, node) {
1299+
try {
1300+
return range.intersectsNode(node);
1301+
} catch {
1302+
return false;
1303+
}
1304+
}
1305+
1306+
function textNodeSelectionOffsets(range, node) {
1307+
const length = node.textContent.length;
1308+
let start = 0;
1309+
let end = length;
1310+
1311+
if (node === range.startContainer) {
1312+
start = range.startOffset;
1313+
}
1314+
if (node === range.endContainer) {
1315+
end = range.endOffset;
1316+
}
1317+
1318+
return {
1319+
start: Math.max(0, Math.min(length, start)),
1320+
end: Math.max(0, Math.min(length, end)),
1321+
};
1322+
}
1323+
1324+
function segmentsFromRange(range) {
1325+
const grouped = new Map();
1326+
const walker = document.createTreeWalker(els.article, NodeFilter.SHOW_TEXT);
1327+
1328+
while (walker.nextNode()) {
1329+
const node = walker.currentNode;
1330+
if (!node.textContent || !rangeIntersectsNode(range, node)) continue;
1331+
1332+
const block = findAnnotationBlock(node);
1333+
if (!block?.dataset.blockId) continue;
1334+
1335+
const { start, end } = textNodeSelectionOffsets(range, node);
1336+
if (end <= start || !node.textContent.slice(start, end).trim()) continue;
1337+
1338+
const startOffset = offsetInBlock(block, node, start);
1339+
const endOffset = offsetInBlock(block, node, end);
1340+
const chapterIndex = Number(block.dataset.chapterIndex);
1341+
if (startOffset < 0 || endOffset <= startOffset || !Number.isInteger(chapterIndex)) continue;
1342+
1343+
const key = block.dataset.blockId;
1344+
const existing = grouped.get(key);
1345+
if (existing) {
1346+
existing.startOffset = Math.min(existing.startOffset, startOffset);
1347+
existing.endOffset = Math.max(existing.endOffset, endOffset);
1348+
} else {
1349+
grouped.set(key, {
1350+
block,
1351+
blockId: key,
1352+
chapterIndex,
1353+
startOffset,
1354+
endOffset,
1355+
});
1356+
}
1357+
}
1358+
1359+
return [...grouped.values()]
1360+
.map((segment) => ({
1361+
...segment,
1362+
quote: segment.block.textContent.slice(segment.startOffset, segment.endOffset),
1363+
}))
1364+
.filter((segment) => segment.quote.trim());
1365+
}
1366+
1367+
function storedAnnotationSegments(segments) {
1368+
return segments.map(({ block, ...segment }) => segment);
1369+
}
1370+
1371+
function annotationSegments(annotation) {
1372+
if (Array.isArray(annotation.segments) && annotation.segments.length) {
1373+
return annotation.segments;
1374+
}
1375+
1376+
return [
1377+
{
1378+
blockId: annotation.blockId,
1379+
chapterIndex: annotation.chapterIndex,
1380+
startOffset: annotation.startOffset,
1381+
endOffset: annotation.endOffset,
1382+
quote: annotation.quote,
1383+
},
1384+
];
1385+
}
1386+
12981387
function annotationStyleLabel(style) {
12991388
const text = copy[state.lang];
13001389
if (style === "underline") return text.annotationUnderline;
@@ -1328,7 +1417,7 @@ function collectAnnotatableBlocks(section = null) {
13281417
});
13291418
});
13301419

1331-
return blocks.filter((block) => els.article.contains(block) && !block.closest("pre, .mermaid"));
1420+
return blocks.filter((block) => els.article.contains(block) && !block.closest(".mermaid"));
13321421
}
13331422

13341423
function assignAnnotatableBlocks() {
@@ -1343,7 +1432,7 @@ function assignAnnotatableBlocks() {
13431432
function findAnnotationBlock(node) {
13441433
const element = node?.nodeType === Node.TEXT_NODE ? node.parentElement : node;
13451434
if (!element || !els.article.contains(element)) return null;
1346-
if (element.closest("pre, .mermaid")) return null;
1435+
if (element.closest(".mermaid")) return null;
13471436

13481437
const block = element.closest(ANNOTATABLE_SELECTOR);
13491438
return block && els.article.contains(block) ? block : null;
@@ -1408,18 +1497,18 @@ function rangeFromOffsets(block, startOffset, endOffset) {
14081497
return null;
14091498
}
14101499

1411-
function locateAnnotation(annotation) {
1412-
const quote = annotation.quote || "";
1500+
function locateAnnotationSegment(annotation, segment) {
1501+
const quote = segment.quote || "";
14131502
const trimmedQuote = quote.trim();
1414-
const block = annotation.blockId
1415-
? els.article.querySelector(`[data-block-id="${cssEscape(annotation.blockId)}"]`)
1503+
const block = segment.blockId
1504+
? els.article.querySelector(`[data-block-id="${cssEscape(segment.blockId)}"]`)
14161505
: null;
14171506

14181507
function locateInBlock(candidate) {
14191508
if (!candidate) return null;
14201509
const text = candidate.textContent || "";
1421-
const start = Number(annotation.startOffset);
1422-
const end = Number(annotation.endOffset);
1510+
const start = Number(segment.startOffset);
1511+
const end = Number(segment.endOffset);
14231512

14241513
if (Number.isFinite(start) && Number.isFinite(end) && text.slice(start, end) === quote) {
14251514
return { block: candidate, startOffset: start, endOffset: end };
@@ -1445,7 +1534,8 @@ function locateAnnotation(annotation) {
14451534
const direct = locateInBlock(block);
14461535
if (direct) return direct;
14471536

1448-
const section = state.chapterSections[annotation.chapterIndex - 1] || null;
1537+
const sectionIndex = Number(segment.chapterIndex) || Number(annotation.chapterIndex);
1538+
const section = state.chapterSections[sectionIndex - 1] || null;
14491539
for (const candidate of collectAnnotatableBlocks(section)) {
14501540
const located = locateInBlock(candidate);
14511541
if (located) return located;
@@ -1454,8 +1544,8 @@ function locateAnnotation(annotation) {
14541544
return null;
14551545
}
14561546

1457-
function applyAnnotationMark(annotation) {
1458-
const located = locateAnnotation(annotation);
1547+
function applyAnnotationMark(annotation, segment) {
1548+
const located = locateAnnotationSegment(annotation, segment);
14591549
if (!located) return false;
14601550

14611551
const range = rangeFromOffsets(located.block, located.startOffset, located.endOffset);
@@ -1546,12 +1636,18 @@ function renderAnnotations() {
15461636
if (!els.article || els.article.classList.contains("loading")) return;
15471637

15481638
clearAnnotationMarks();
1549-
[...state.annotations]
1639+
const markSegments = state.annotations.flatMap((annotation) =>
1640+
annotationSegments(annotation).map((segment) => ({ annotation, segment })),
1641+
);
1642+
1643+
markSegments
15501644
.sort((a, b) => {
1551-
if (a.blockId === b.blockId) return Number(b.startOffset) - Number(a.startOffset);
1552-
return String(a.blockId).localeCompare(String(b.blockId));
1645+
if (a.segment.blockId === b.segment.blockId) {
1646+
return Number(b.segment.startOffset) - Number(a.segment.startOffset);
1647+
}
1648+
return String(a.segment.blockId).localeCompare(String(b.segment.blockId));
15531649
})
1554-
.forEach(applyAnnotationMark);
1650+
.forEach(({ annotation, segment }) => applyAnnotationMark(annotation, segment));
15551651
renderAnnotationList();
15561652
}
15571653

@@ -1568,21 +1664,13 @@ function createAnnotation(style) {
15681664
return;
15691665
}
15701666

1571-
const startBlock = findAnnotationBlock(range.startContainer);
1572-
const endBlock = findAnnotationBlock(range.endContainer);
1573-
if (!startBlock || !endBlock || startBlock !== endBlock) {
1667+
const segments = segmentsFromRange(range);
1668+
if (!segments.length) {
15741669
setAnnotationStatus(text.annotationSameBlock);
15751670
return;
15761671
}
15771672

1578-
const startOffset = offsetInBlock(startBlock, range.startContainer, range.startOffset);
1579-
const endOffset = offsetInBlock(startBlock, range.endContainer, range.endOffset);
1580-
if (startOffset < 0 || endOffset <= startOffset) {
1581-
setAnnotationStatus(text.annotationNeedSelection);
1582-
return;
1583-
}
1584-
1585-
const quote = startBlock.textContent.slice(startOffset, endOffset);
1673+
const quote = segments.map((segment) => segment.quote.trim()).filter(Boolean).join("\n\n");
15861674
if (!quote.trim()) {
15871675
setAnnotationStatus(text.annotationNeedSelection);
15881676
return;
@@ -1595,7 +1683,8 @@ function createAnnotation(style) {
15951683
comment = entered.trim();
15961684
}
15971685

1598-
const chapterIndex = Number(startBlock.dataset.chapterIndex);
1686+
const firstSegment = segments[0];
1687+
const chapterIndex = Number(firstSegment.chapterIndex);
15991688
if (!Number.isInteger(chapterIndex) || chapterIndex < 1) {
16001689
setAnnotationStatus(text.annotationOutside);
16011690
return;
@@ -1605,11 +1694,12 @@ function createAnnotation(style) {
16051694
id: annotationId(),
16061695
lang: state.lang,
16071696
chapterIndex,
1608-
blockId: startBlock.dataset.blockId,
1697+
blockId: firstSegment.blockId,
16091698
style,
16101699
quote,
1611-
startOffset,
1612-
endOffset,
1700+
startOffset: firstSegment.startOffset,
1701+
endOffset: firstSegment.endOffset,
1702+
segments: storedAnnotationSegments(segments),
16131703
comment,
16141704
createdAt: new Date().toISOString(),
16151705
};
@@ -2754,6 +2844,8 @@ els.resetMatrix.addEventListener("click", resetMatrixProgress);
27542844
els.search.addEventListener("input", renderToc);
27552845
els.top.addEventListener("click", () => window.scrollTo({ top: 0, behavior: "smooth" }));
27562846
els.annotationPopover.addEventListener("mousedown", (event) => event.preventDefault());
2847+
els.annotationPopover.addEventListener("pointerdown", (event) => event.preventDefault());
2848+
els.annotationPopover.addEventListener("touchstart", (event) => event.preventDefault());
27572849
els.annotationPopover.addEventListener("click", (event) => event.stopPropagation());
27582850
els.highlightButton.addEventListener("click", () => createAnnotation("highlight"));
27592851
els.underlineButton.addEventListener("click", () => createAnnotation("underline"));

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
<meta name="twitter:card" content="summary" />
2020
<link rel="canonical" href="https://lling0000.github.io/Vibe_coding_guide/" />
2121
<title>Vibe Coding Guide | AI Coding 工程工作流手册</title>
22-
<link rel="stylesheet" href="./assets/styles.css?v=20260618-selection-popover" />
22+
<link rel="stylesheet" href="./assets/styles.css?v=20260618-annotation-all-ranges" />
2323
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
2424
<script defer src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
2525
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>
2626
<script defer src="https://cdn.jsdelivr.net/npm/lucide@0.475.0/dist/umd/lucide.min.js"></script>
27-
<script defer src="./assets/site.js?v=20260618-selection-popover"></script>
27+
<script defer src="./assets/site.js?v=20260618-annotation-all-ranges"></script>
2828
</head>
2929
<body data-mode="planner">
3030
<div class="site-progress" aria-hidden="true">

0 commit comments

Comments
 (0)