From 97e546b2a4cd35e0d1ed8978fc602bc162264f58 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 18 Aug 2026 19:13:51 -0400 Subject: [PATCH] feat(reader): send end-anchor CFI for EPUB highlights Web highlights stored only epubcfi_start, so devices received degenerate pos0 == pos1 (zero-length) highlight ranges. The reader now collapses the selection range to its end point for a second CFI and stores it as epubcfi_end (PDF rect anchors reuse the JSON anchor for both ends). --- web/src/reader/reader.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index 6eedafd..4c94e29 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -412,6 +412,7 @@ document.addEventListener("alpine:init", () => { note: string; color: string; cfi: string; + cfiEnd: string; percentage: number; pdfPage: number; pdfRects: number[][]; @@ -443,6 +444,7 @@ document.addEventListener("alpine:init", () => { y: 0, text: "", cfi: "", + cfiEnd: "", id: "", color: "#ffd54f", note: "", @@ -701,8 +703,15 @@ document.addEventListener("alpine:init", () => { const text = sel.toString().replace(/\s+/g, " ").trim(); if (!text) return; let cfi: string; + let cfiEnd: string; try { cfi = this.view.getCFI(index, range); + // Collapse to the end point for a distinct end anchor — + // KOReader sync renders the highlight box from pos0/pos1, and + // pos1 == pos0 would be a degenerate (zero-length) range. + const endRange = range.cloneRange(); + endRange.collapse(false); + cfiEnd = this.view.getCFI(index, endRange); } catch { return; } @@ -715,6 +724,7 @@ document.addEventListener("alpine:init", () => { y: (iframeRect?.top ?? 0) + rect.top, text, cfi, + cfiEnd, }); }; doc.addEventListener( @@ -827,6 +837,7 @@ document.addEventListener("alpine:init", () => { y: (iframeRect?.top ?? 0) + rect.top, text: h.text, cfi: h.cfi, + cfiEnd: h.cfiEnd, id: h.id, color: h.color, note: h.note, @@ -1070,6 +1081,7 @@ document.addEventListener("alpine:init", () => { y: number; text: string; cfi: string; + cfiEnd?: string; id?: string; color?: string; note?: string; @@ -1080,6 +1092,7 @@ document.addEventListener("alpine:init", () => { p.mode = opts.mode; p.text = opts.text; p.cfi = opts.cfi; + p.cfiEnd = opts.cfiEnd ?? ""; p.id = opts.id ?? ""; p.color = opts.color || "#ffd54f"; p.note = opts.note ?? ""; @@ -1147,6 +1160,7 @@ document.addEventListener("alpine:init", () => { note: r.note_text ?? "", color: r.color ?? "#ffff00", cfi, + cfiEnd: r.epubcfi_end ?? "", percentage: r.percentage_start ?? 0, pdfPage, pdfRects, @@ -1205,6 +1219,7 @@ document.addEventListener("alpine:init", () => { start_position: "", end_position: "", epubcfi_start: p.pdfPage >= 0 ? pdfAnchor : p.cfi, + epubcfi_end: p.pdfPage >= 0 ? pdfAnchor : p.cfiEnd, color, note_text: "", percentage_start: this.lastRelocateDetail?.fraction ?? 0, @@ -1263,6 +1278,7 @@ document.addEventListener("alpine:init", () => { start_position: "", end_position: "", epubcfi_start: anchor, + epubcfi_end: p.pdfPage >= 0 ? anchor : p.cfiEnd, color: p.color, note_text: p.note, }),