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;
|
color: string;
|
||||||
cfi: string;
|
cfi: string;
|
||||||
cfiEnd: string;
|
cfiEnd: string;
|
||||||
|
renderCfi: string;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
pdfPage: number;
|
pdfPage: number;
|
||||||
pdfRects: number[][];
|
pdfRects: number[][];
|
||||||
@@ -1122,7 +1123,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
} else {
|
} else {
|
||||||
this.view
|
this.view
|
||||||
?.addAnnotation({
|
?.addAnnotation({
|
||||||
value: hl.cfi,
|
value: hl.renderCfi || hl.cfi,
|
||||||
color: hl.color,
|
color: hl.color,
|
||||||
note: hl.note,
|
note: hl.note,
|
||||||
id: hl.id,
|
id: hl.id,
|
||||||
@@ -1154,18 +1155,68 @@ document.addEventListener("alpine:init", () => {
|
|||||||
/* not ours; leave as-is */
|
/* not ours; leave as-is */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const cfiEnd = r.epubcfi_end ?? "";
|
||||||
return {
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
text: r.selection_text ?? "",
|
text: r.selection_text ?? "",
|
||||||
note: r.note_text ?? "",
|
note: r.note_text ?? "",
|
||||||
color: r.color ?? "#ffff00",
|
color: r.color ?? "#ffff00",
|
||||||
cfi,
|
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,
|
percentage: r.percentage_start ?? 0,
|
||||||
pdfPage,
|
pdfPage,
|
||||||
pdfRects,
|
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() {
|
async refreshAnnotations() {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token || !this.mediaItemId) return;
|
if (!token || !this.mediaItemId) return;
|
||||||
@@ -1245,7 +1296,10 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.view?.addAnnotation({
|
this.view?.addAnnotation({
|
||||||
value: p.cfi,
|
value:
|
||||||
|
p.pdfPage >= 0
|
||||||
|
? ""
|
||||||
|
: this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi,
|
||||||
color,
|
color,
|
||||||
note: "",
|
note: "",
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -1298,7 +1352,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.view?.addAnnotation({
|
this.view?.addAnnotation({
|
||||||
value: p.cfi,
|
value: this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi,
|
||||||
color: p.color,
|
color: p.color,
|
||||||
note: p.note,
|
note: p.note,
|
||||||
id: p.id,
|
id: p.id,
|
||||||
@@ -1340,6 +1394,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
goToHighlight(hl: {
|
goToHighlight(hl: {
|
||||||
cfi: string;
|
cfi: string;
|
||||||
|
renderCfi: string;
|
||||||
pdfPage: number;
|
pdfPage: number;
|
||||||
}) {
|
}) {
|
||||||
if (hl.pdfPage >= 0) {
|
if (hl.pdfPage >= 0) {
|
||||||
@@ -1347,9 +1402,11 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.pushBackStack();
|
this.pushBackStack();
|
||||||
this.view?.goTo?.(hl.pdfPage);
|
this.view?.goTo?.(hl.pdfPage);
|
||||||
this.closeDrawers();
|
this.closeDrawers();
|
||||||
} else if (hl.cfi) {
|
} else if (hl.renderCfi || hl.cfi) {
|
||||||
this.pushBackStack();
|
this.pushBackStack();
|
||||||
this.view?.showAnnotation({ value: hl.cfi })?.catch?.(() => {});
|
this.view
|
||||||
|
?.showAnnotation({ value: hl.renderCfi || hl.cfi })
|
||||||
|
?.catch?.(() => {});
|
||||||
this.closeDrawers();
|
this.closeDrawers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user