fix(reader): render device-synced highlights — synthesize range CFIs
Device-synced highlights stored POINT CFIs (epubcfi(.../8/1:1)); the overlayer resolves those to a collapsed range and paints nothing, so KOReader-made highlights were listed in the drawer but invisible on the page. mapHighlightRow now builds a renderCfi: a proper RANGE CFI (epubcfi(base,/start,/end)) synthesized from the stored start/end points. It also repairs stale rows: missing ends (old web highlights) and degenerate document-start ends (the old converter fallback) are derived from the start offset plus the selection text's UTF-16 length. All overlay drawing, navigation (showAnnotation), and the post-create/post-edit re-adds use renderCfi. Verified in-browser against live device-synced rows: the paginator's overlayer paints the highlight rects after the fix.
This commit is contained in:
@@ -413,6 +413,7 @@ document.addEventListener("alpine:init", () => {
|
||||
color: string;
|
||||
cfi: string;
|
||||
cfiEnd: string;
|
||||
renderCfi: string;
|
||||
percentage: number;
|
||||
pdfPage: number;
|
||||
pdfRects: number[][];
|
||||
@@ -1122,7 +1123,7 @@ document.addEventListener("alpine:init", () => {
|
||||
} else {
|
||||
this.view
|
||||
?.addAnnotation({
|
||||
value: hl.cfi,
|
||||
value: hl.renderCfi || hl.cfi,
|
||||
color: hl.color,
|
||||
note: hl.note,
|
||||
id: hl.id,
|
||||
@@ -1154,18 +1155,68 @@ document.addEventListener("alpine:init", () => {
|
||||
/* not ours; leave as-is */
|
||||
}
|
||||
}
|
||||
const cfiEnd = r.epubcfi_end ?? "";
|
||||
return {
|
||||
id: r.id,
|
||||
text: r.selection_text ?? "",
|
||||
note: r.note_text ?? "",
|
||||
color: r.color ?? "#ffff00",
|
||||
cfi,
|
||||
cfiEnd: r.epubcfi_end ?? "",
|
||||
cfiEnd,
|
||||
// Rendering/navigating anchor: device-synced highlights store
|
||||
// POINT CFIs (epubcfi(/6/N!/4/2[id]/8/1:1)), which resolve to a
|
||||
// collapsed range and paint nothing. Foliate's overlayer needs a
|
||||
// RANGE CFI — same shape getCFI() produces natively
|
||||
// (epubcfi(/6/N!/4/2[id],/8/1:1,/8/1:67)) — synthesized here from
|
||||
// the stored start and end points when both share a base path.
|
||||
renderCfi: this.toRangeCfi(cfi, cfiEnd, r.selection_text ?? ""),
|
||||
percentage: r.percentage_start ?? 0,
|
||||
pdfPage,
|
||||
pdfRects,
|
||||
};
|
||||
},
|
||||
// Build a foliate-renderable RANGE CFI from stored (possibly point)
|
||||
// CFIs. Repairs two stale shapes using the selection text: a missing
|
||||
// end (old web highlights), and a degenerate end — the device-push
|
||||
// converter used to fall back to a document-start CFI when the end
|
||||
// xpointer didn't resolve exactly. In both cases the end is derived
|
||||
// from the start offset advanced by the text's UTF-16 length (EPUB
|
||||
// CFI offsets are UTF-16 code units); multi-node selections just fail
|
||||
// resolution harmlessly and fall back to the point CFI.
|
||||
toRangeCfi(start: string, end: string, text: string): string {
|
||||
if (!start) return end || start;
|
||||
if (start.includes(",")) return start; // already a range CFI
|
||||
const re =
|
||||
/^(epubcfi\(\/\d+\/\d+!\/\d+\/\d+(?:\[[^\]]*\])?)(\/(?:[^:)]+)?(?::(\d+))?)\)$/;
|
||||
const ms = re.exec(start);
|
||||
if (!ms) return start;
|
||||
const base = ms[1];
|
||||
const startLocal = ms[2];
|
||||
const startOff = ms[3] ? parseInt(ms[3], 10) : -1;
|
||||
const utf16len = [...(text ?? "")].reduce(
|
||||
(n, c) => n + (c.codePointAt(0)! > 0xffff ? 2 : 1),
|
||||
0,
|
||||
);
|
||||
let endLocal = "";
|
||||
if (end && !end.includes(",")) {
|
||||
const me = re.exec(end);
|
||||
if (me && me[1] === base) {
|
||||
const endOff = me[3] ? parseInt(me[3], 10) : -1;
|
||||
// Degenerate: end resolves to the document start (the old
|
||||
// converter fallback) or sits before the start offset.
|
||||
const degenerate =
|
||||
endOff === 0 ||
|
||||
(startOff >= 0 && endOff >= 0 && endOff < startOff);
|
||||
if (!degenerate) endLocal = me[2];
|
||||
}
|
||||
}
|
||||
if (!endLocal) {
|
||||
if (startOff < 0 || utf16len <= 0) return start; // point CFI
|
||||
const cut = startLocal.lastIndexOf(":");
|
||||
endLocal = `${startLocal.slice(0, cut)}:${startOff + utf16len}`;
|
||||
}
|
||||
return `${base},${startLocal},${endLocal})`;
|
||||
},
|
||||
async refreshAnnotations() {
|
||||
const token = getToken();
|
||||
if (!token || !this.mediaItemId) return;
|
||||
@@ -1245,7 +1296,10 @@ document.addEventListener("alpine:init", () => {
|
||||
}
|
||||
} else {
|
||||
this.view?.addAnnotation({
|
||||
value: p.cfi,
|
||||
value:
|
||||
p.pdfPage >= 0
|
||||
? ""
|
||||
: this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi,
|
||||
color,
|
||||
note: "",
|
||||
id: row.id,
|
||||
@@ -1298,7 +1352,7 @@ document.addEventListener("alpine:init", () => {
|
||||
});
|
||||
} else {
|
||||
this.view?.addAnnotation({
|
||||
value: p.cfi,
|
||||
value: this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi,
|
||||
color: p.color,
|
||||
note: p.note,
|
||||
id: p.id,
|
||||
@@ -1340,6 +1394,7 @@ document.addEventListener("alpine:init", () => {
|
||||
},
|
||||
goToHighlight(hl: {
|
||||
cfi: string;
|
||||
renderCfi: string;
|
||||
pdfPage: number;
|
||||
}) {
|
||||
if (hl.pdfPage >= 0) {
|
||||
@@ -1347,9 +1402,11 @@ document.addEventListener("alpine:init", () => {
|
||||
this.pushBackStack();
|
||||
this.view?.goTo?.(hl.pdfPage);
|
||||
this.closeDrawers();
|
||||
} else if (hl.cfi) {
|
||||
} else if (hl.renderCfi || hl.cfi) {
|
||||
this.pushBackStack();
|
||||
this.view?.showAnnotation({ value: hl.cfi })?.catch?.(() => {});
|
||||
this.view
|
||||
?.showAnnotation({ value: hl.renderCfi || hl.cfi })
|
||||
?.catch?.(() => {});
|
||||
this.closeDrawers();
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user